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.
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 exchange code
- Failed to receive authorization code
- Failed to receive authorization code
- Download failed
- Failed to fetch JWKS
AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16).
Data as JSON: /api/errors/27590a76aac185df.
Report an issue: GitHub.
Appendix: 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)