aaif-goose/goose · error
Failed to exchange code: {} - {}
Error message
Failed to exchange code: {} - {} What it means
After the OpenRouter OAuth callback, the PKCE authorization code is exchanged at OPENROUTER_TOKEN_URL together with the flow's code_verifier. A non-success HTTP status surfaces as this error carrying the status code and response body (both also printed to stderr), so the failure detail from the token endpoint is preserved.
Source
Thrown at crates/goose/src/config/signup_openrouter/mod.rs:122
eprintln!("Exchanging code for API key...");
eprintln!("Code: {}", code);
eprintln!("Code verifier length: {}", self.code_verifier.len());
eprintln!("Code challenge: {}", self.code_challenge);
let response = client
.post(OPENROUTER_TOKEN_URL)
.json(&request_body)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
eprintln!("Token exchange failed!");
eprintln!("Status: {}", status);
eprintln!("Error response: {}", error_text);
return Err(anyhow!(
"Failed to exchange code: {} - {}",
status,
error_text
));
}
let token_response: TokenResponse = response.json().await?;
Ok(token_response.key)
}
/// Complete flow: open browser, wait for callback, exchange code
pub async fn complete_flow(&mut self) -> Result<String> {
let auth_url = self.get_auth_url();
println!("Opening browser for authentication...");
eprintln!("Auth URL: {}", auth_url);
if let Err(e) = webbrowser::open(&auth_url) {View on GitHub (pinned to 3810898a74)
Solutions
- Read the status and body in the message/stderr — 400 with invalid_grant means the code is spent or expired: restart the complete_flow() from scratch to get a fresh code+verifier pair
- Ensure the exchange runs exactly once per code, inside the same SignupFlow instance that built the auth URL
- For transient 5xx/network failures, wait briefly and retry the whole flow
Defensive patterns
Strategy: retry
Try / catch
match flow.exchange_code(code).await {
Err(e) if e.to_string().starts_with("Failed to exchange code") => {
// status+body are in the message. invalid_grant/expired => restart complete_flow()
// for a fresh code+verifier; transient 5xx => back off briefly and rerun the flow
}
other => other?,
} Prevention
- Always exchange within the same SignupFlow instance that generated the auth URL so the PKCE verifier matches
- Exchange each code exactly once, immediately after receiving the callback
- Keep the exchange within the code's short validity window
When it happens
Trigger: POST to the token endpoint returns 4xx/5xx: code already redeemed or expired (400 invalid_grant), code_verifier not matching this code (each SignupFlow generates its own verifier), clock skew, or an upstream 5xx.
Common situations: Retrying the exchange with a code that was already used; reusing a stale flow object after an interrupted signup; network flakiness or OpenRouter outage during exchange.
Related errors
- Failed to receive authorization code
- Failed to exchange code: {} - {}
- Failed to receive authorization code
- HTTP error! status: ${response.status}
- Failed to fetch OIDC config: ${configResp.status}
AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16).
Data as JSON: /api/errors/27590a76aac185df.
Report an issue: GitHub.