sigoden/aichat · critical
{err_msg}
Error message
{err_msg} What it means
Thrown by fetch_access_token in src/client/vertexai.rs:485 when the OAuth2 token endpoint responds with a JSON body containing an `error_description` field instead of a valid access_token/expires_in pair. The library propagates Google's own error description verbatim as the error message. This is an authentication failure — Google rejected the token exchange for the service account credentials being used.
Solutions
- Read the error_description in the message (e.g. "invalid_grant") and fix the underlying Google auth problem it names.
- Regenerate the service account key and update the credentials JSON / GOOGLE_APPLICATION_CREDENTIALS path.
- Check the system clock — significant skew invalidates the signed JWT; run NTP sync.
- Verify the service account exists and is enabled in the Google Cloud console with correct IAM roles.
- Re-run gcloud auth application-default login if relying on Application Default Credentials.
Example fix
// before (stale key in credentials.json) // error: Invalid JWT Signature. // after // regenerate key: // gcloud iam service-accounts keys create new-key.json \ // --iam-account=my-sa@project.iam.gserviceaccount.com // then set GOOGLE_APPLICATION_CREDENTIALS=/path/to/new-key.json
Defensive patterns
Strategy: retry
Validate before calling
// Validate the ADC credentials file before attempting the token exchange
fn validate_adc_file(path: &str) -> anyhow::Result<()> {
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path)?)?;
let key_ok = v["private_key"].as_str().map_or(false, |k| k.contains("BEGIN PRIVATE KEY"));
let acct_ok = v["client_email"].as_str().map_or(false, |e| e.ends_with(".iam.gserviceaccount.com"));
anyhow::ensure!(key_ok && acct_ok, "credentials file missing valid private_key/client_email");
Ok(())
} Try / catch
let token = loop {
match prepare_gcloud_access_token().await {
Ok(t) => break t,
Err(e) if is_invalid_grant(&e.to_string()) => {
rotate_service_account_key()?; // or alert ops, then retry once
attempts += 1;
if attempts > 1 { return Err(e.into()); }
}
Err(e) => return Err(e.into()),
}
}; Prevention
- Rotate service account keys on a schedule and update credentials files before old keys expire.
- Keep the host clock accurate via NTP — clock skew invalidates signed JWTs (invalid_grant).
- Verify GOOGLE_APPLICATION_CREDENTIALS points to a current, enabled service account key file.
- Check the Google Cloud console that the service account exists, is enabled, and has needed IAM roles.
- Surface the error_description from the message to ops tooling for fast diagnosis of auth failures.
When it happens
Trigger: Calling prepare_gcloud_access_token -> fetch_access_token where the token endpoint returns e.g. {"error":"invalid_grant","error_description":"Invalid JWT Signature."} — caused by expired/rotated service account keys, wrong private_key in the credentials JSON, badly skewed system clock, or the service account being disabled/deleted.
Common situations: Rotated or deleted service account keys while old GOOGLE_APPLICATION_CLOUD credentials JSON still on disk; misconfigured GOOGLE_APPLICATION_CREDENTIALS pointing to the wrong file; clock drift on the machine invalidating signed JWTs; disabled service accounts or missing IAM roles on the project.
Related errors
- Invalid response data
- Invalid access token
- Failed to parse expires_in of access_token
- Invalid response data
- The model does not support network images
AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09).
Data as JSON: /api/errors/980279e0d51cf279.
Report an issue: GitHub.
Appendix: source
Thrown at src/client/vertexai.rs:485
async fn fetch_access_token(
client: &reqwest::Client,
file: &Option<String>,
) -> Result<(String, i64)> {
let credentials = load_adc(file).await?;
let value: Value = client
.post("https://oauth2.googleapis.com/token")
.json(&credentials)
.send()
.await?
.json()
.await?;
if let (Some(access_token), Some(expires_in)) =
(value["access_token"].as_str(), value["expires_in"].as_i64())
{
Ok((access_token.to_string(), expires_in))
} else if let Some(err_msg) = value["error_description"].as_str() {
bail!("{err_msg}")
} else {
bail!("Invalid response data: {value}")
}
}
async fn load_adc(file: &Option<String>) -> Result<Value> {
let adc_file = file
.as_ref()
.map(PathBuf::from)
.or_else(default_adc_file)
.ok_or_else(|| anyhow!("No application_default_credentials.json"))?;
let data = tokio::fs::read_to_string(adc_file).await?;
let data: Value = serde_json::from_str(&data)?;
if let (Some(client_id), Some(client_secret), Some(refresh_token)) = (
data["client_id"].as_str(),
data["client_secret"].as_str(),
data["refresh_token"].as_str(),
) {View on GitHub (pinned to 82976d349a)