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

  1. 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
  2. Ensure the exchange runs exactly once per code, inside the same SignupFlow instance that built the auth URL
  3. 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

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


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/27590a76aac185df. Report an issue: GitHub.