nexu-io/open-design · error · Error
token endpoint rejected request: HTTP ${res.status} ${res.st
Error message
token endpoint rejected request: HTTP ${res.status} ${res.statusText} ${txt} What it means
exchangeCodeForToken POSTs the authorization code plus PKCE verifier to the token endpoint. A non-2xx response fails with status, statusText, and up to 500 chars of body. Common reasons: expired or already-used auth code, wrong redirectUri, bad client_secret, PKCE mismatch, or invalid_grant.
Source
Thrown at apps/daemon/src/mcp-oauth.ts:424
const headers: Record<string, string> = {
'content-type': 'application/x-www-form-urlencoded',
accept: 'application/json',
};
if (clientSecret) {
// RFC 6749 §2.3.1 — confidential clients use HTTP Basic with the
// client_id we already put in the form. Public clients (PKCE-only)
// skip this branch.
const basic = Buffer.from(`${form.get('client_id')}:${clientSecret}`).toString('base64');
headers['authorization'] = `Basic ${basic}`;
}
const res = await fetchImpl(tokenEndpoint, {
method: 'POST',
headers,
body: form.toString(),
});
if (!res.ok) {
const txt = await safeText(res);
throw new Error(
`token endpoint rejected request: HTTP ${res.status} ${res.statusText} ${txt}`,
);
}
const json = (await res.json()) as OAuthTokenResponse;
if (!json.access_token) {
throw new Error('token endpoint response missing access_token');
}
return json;
}
async function safeText(res: Response): Promise<string> {
try {
const t = await res.text();
return t.slice(0, 500);
} catch {
return '';
}
}View on GitHub (pinned to 5be4028344)
Solutions
- Read the status and body in the message to distinguish invalid_grant from auth failure.
- Ensure the auth code is exchanged exactly once and promptly.
- Keep the same redirectUri and PKCE verifier used in beginAuth.
- For confidential clients, confirm client_secret.
Defensive patterns
Strategy: retry
Try / catch
try {
await exchangeCodeForToken(tokenEndpoint, params);
} catch (e) {
if (/token endpoint rejected/i.test(e.message)) {
// a replayed or expired code yields invalid_grant -> restart the flow
if (/invalid_grant/i.test(e.message)) return beginAuth(input);
}
throw e;
} Prevention
- Persist the PKCE verifier across the redirect.
- Guard the OAuth callback against double-invocation.
- Keep redirectUri identical between authorize and token exchange.
When it happens
Trigger: Replaying a single-use auth code; code expired; redirectUri in the token request differs from the authorize step; PKCE verifier mismatch; confidential client with the wrong secret.
Common situations: The OAuth callback was hit twice (retry or double-click); clock skew; the verifier was not preserved between beginAuth and the callback.
Related errors
- dynamic client registration failed: HTTP ${res.status} ${res
- token endpoint response missing access_token
- could not discover OAuth metadata for ${issuer}
- Could not fetch ${url} — the site may block server-side requ
- invalid brand asset url: ${String(url)}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/cff65e217018390b.
Report an issue: GitHub.