mastra-ai/mastra · error
GitHub OAuth token exchange returned no token
Error message
GitHub OAuth token exchange returned no token: ${data.error_description ?? data.error ?? 'unknown'} What it means
Thrown by the GitHub OAuth token exchange helper when GitHub's access_token endpoint responds 200 but the JSON body has no access_token. GitHub returns error/error_description fields in the body instead of a token when the authorization code, client credentials, or redirect URI is invalid. The library surfaces that error text so the developer can see why GitHub refused the exchange.
Solutions
- Log the full response body from GitHub and fix the reported error_description (e.g. bad_verification_code means the code was reused or expired).
- Verify GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, and redirect_uri exactly match the GitHub OAuth app settings.
- Generate a fresh authorization code for each exchange; never retry with the same code.
- Check the GitHub OAuth app is not suspended and has not hit rate limits.
- Re-authenticate end-to-end rather than caching exchange responses.
Example fix
// before: reusing a stored code
await exchangeGithubToken({ code: savedCode });
// after: use a fresh code per exchange and check the error body first
const data = await fetchTokenEndpoint({ code: freshCode });
if (!data.access_token) throw new Error(`OAuth failed: ${data.error_description ?? data.error}`); Defensive patterns
Strategy: try-catch
Validate before calling
// check config before exchanging
if (!process.env.GITHUB_CLIENT_ID || !process.env.GITHUB_CLIENT_SECRET) throw new Error('OAuth app credentials missing'); Type guard
function hasAccessToken(d: unknown): d is { access_token: string } {
return typeof d === 'object' && d !== null && 'access_token' in d && typeof (d as any).access_token === 'string';
} Try / catch
try {
const token = await exchangeGithubToken({ code });
} catch (e) {
// message includes GitHub's error_description; treat bad_verification_code as re-auth required
console.error('OAuth exchange failed:', (e as Error).message);
return redirect('/login'); // force a fresh code
} Prevention
- Never reuse an authorization code; always start a fresh authorize redirect.
- Keep redirect_uri identical in authorize and token steps.
- Verify OAuth app client id/secret env vars at startup.
When it happens
Trigger: Calling the OAuth token exchange with an expired or already-consumed authorization code, a code_verifier/client secret mismatch, a redirect_uri that differs from the one used in the authorize step, or GitHub rate-limiting the OAuth app (body contains error but no token).
Common situations: Users re-authorizing after the code was redeemed once; misconfigured GITHUB_CLIENT_ID/SECRET env vars; localhost callback URL not registered on the GitHub OAuth app; clock/replay issues causing single-use code reuse.
Related errors
- AZURE_AD_TOKEN_ERROR
- Failed to load GitHub token status
- GitHub capabilities require an app-installation connection.
- GitHub OAuth token exchange failed
- GitHub subscriptions require an authenticated repository…
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/ee57fc27600fff9d.
Report an issue: GitHub.
Appendix: source
Thrown at mastracode/factory/src/integrations/github/integration.ts:1127
/** Exchange an OAuth `code` for a user access token. */
async exchangeOAuthCode(code: string, redirectUri: string): Promise<string> {
const res = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
signal: AbortSignal.timeout(GITHUB_OAUTH_TOKEN_TIMEOUT_MS),
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({
client_id: this.#clientId,
client_secret: this.#clientSecret,
code,
redirect_uri: redirectUri,
}),
});
if (!res.ok) {
throw new Error(`GitHub OAuth token exchange failed: ${res.status}`);
}
const data = (await res.json()) as { access_token?: string; error?: string; error_description?: string };
if (!data.access_token) {
throw new Error(
`GitHub OAuth token exchange returned no token: ${data.error_description ?? data.error ?? 'unknown'}`,
);
}
return data.access_token;
}
/**
* The integration's HTTP surface: the `/web/github/*` + `/auth/github/*`
* Mastra `apiRoutes` (webhook handler, install/OAuth flow, project +
* worktree + session operations). The factory folds these into the server's
* `apiRoutes` when the feature is ready. Handlers operate on this instance.
*/
routes(ctx: IntegrationContext): ApiRoute[] {
this.#storage = ctx.storage;
const ingestFactoryEvent = attachGithubRules(this, ctx);
return buildGithubRoutes({
github: this,
auth: ctx.auth,View on GitHub (pinned to 75dd419e61)