BloopAI/vibe-kanban · error

google token exchange failed: {detail}

Error message

google token exchange failed: {detail}

What it means

During the Google OAuth code-for-token exchange, Google's token endpoint returned an error variant (e.g. invalid_grant, redirect_uri_mismatch) instead of tokens. The provider wraps Google's error code or description into this bail message.

Source

Thrown at crates/remote/src/auth/provider.rs:571

                        (!trimmed.is_empty()).then_some(trimmed.to_string())
                    })
                    .collect();

                Ok(AuthorizationGrant {
                    access_token: SecretString::new(access_token.into()),
                    token_type,
                    scopes,
                    refresh_token: refresh_token.map(|v| SecretString::new(v.into())),
                    expires_in: expires_in.map(Duration::seconds),
                    id_token: id_token.map(|v| SecretString::new(v.into())),
                })
            }
            GoogleTokenResponse::Error {
                error,
                error_description,
            } => {
                let detail = error_description.unwrap_or_else(|| error.clone());
                anyhow::bail!("google token exchange failed: {detail}")
            }
        }
    }

    async fn fetch_user(&self, access_token: &SecretString) -> Result<ProviderUser> {
        let bearer = format!("Bearer {}", access_token.expose_secret());

        let profile: GoogleUser = self
            .client
            .get("https://openidconnect.googleapis.com/v1/userinfo")
            .header("Authorization", bearer)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?;

        let login = profile.email.clone();

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Restart the OAuth flow to get a fresh authorization code and exchange it immediately
  2. Ensure the redirect_uri exactly matches one registered in Google Cloud Console
  3. Verify client_id/client_secret for the correct Google OAuth client/environment
  4. If using offline access, request the correct scopes/access_type and use refresh_token grant appropriately

Example fix

// before
// token request redirect_uri = "http://localhost:3000/callback"
// authorize request redirect_uri = "https://app.example.com/callback" -> invalid_grant / redirect_uri_mismatch
// after
// use the identical redirect_uri in both authorize and token requests
Defensive patterns

Strategy: retry

Try / catch

match provider.exchange_code(&code).await { Err(e) if e.to_string().starts_with("google token exchange failed") => restart_oauth_flow(), other => other? }

Prevention

When it happens

Trigger: exchange_code() on the Google provider receives GoogleTokenResponse::Error — typically because the code is expired/used, or redirect_uri/client credentials don't match the original request.

Common situations: Authorization code expired (Google codes are short-lived, ~10 min) or already redeemed; redirect URI not whitelisted in Google Cloud Console; wrong client secret between environments (staging vs prod); refresh-token flow attempted with the wrong grant type.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/cc8dcaacb1435da4. Report an issue: GitHub.