can1357/oh-my-pi · error · Error

Token exchange returned no access token${providerError ? `:

Error message

Token exchange returned no access token${providerError ? `: ${providerError}` : ""}

What it means

Thrown by exchangeToken in the MCP OAuth flow when the token endpoint responds successfully but the JSON body has no non-empty `access_token`. This catches providers like the Slack Web API that return HTTP 200 with an `{ ok: false, error }` body instead of a real HTTP error, which would otherwise store an empty token that only fails later with `invalid_token`. The message appends the provider's `error_description` or `error` field when present.

Source

Thrown at packages/coding-agent/src/mcp/oauth-flow.ts:533

			const errorText = await response.text();
			throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
		}

		const data = (await response.json()) as {
			access_token?: string;
			refresh_token?: string;
			expires_in?: number;
			token_type?: string;
			error?: string;
			error_description?: string;
		};

		// Some providers (e.g. the Slack Web API) signal failure with HTTP 200 and
		// an `{ ok: false, error }` body. Accepting such a response would store an
		// empty access token and only surface `invalid_token` on a later request.
		if (typeof data.access_token !== "string" || data.access_token.length === 0) {
			const providerError = data.error_description ?? data.error;
			throw new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : ""}`);
		}

		// Calculate expiry timestamp
		const expiresIn = data.expires_in ?? 3600; // Default to 1 hour
		const expires = Date.now() + expiresIn * 1000;

		return {
			access: data.access_token,
			refresh: data.refresh_token ?? "",
			expires,
		};
	}

	/**
	 * Generate PKCE code verifier (random string).
	 */
	#generateCodeVerifier(): string {
		const bytes = new Uint8Array(32);

View on GitHub (pinned to 9690622007)

Solutions

  1. Read the provider error appended after the colon in the message (e.g. `bad_verification_code`, `invalid_client`) and fix the corresponding OAuth parameter
  2. Verify client_id, client_secret, and redirect_uri exactly match the app registration in the provider's dashboard
  3. Re-run the full OAuth flow to get a fresh authorization code — codes are single-use and expire quickly
  4. Check the MCP server's OAuth documentation for non-standard token endpoint behavior

Example fix

// before: blindly trusting any 200 response
const data = await response.json();
await storeToken(data.access_token);
// after: validate before storing (this is what the library does)
if (typeof data.access_token !== "string" || data.access_token.length === 0) {
  throw new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : ""}`);
}
Defensive patterns

Strategy: validation

Validate before calling

// validate before calling the OAuth flow
const clientId = config.clientId?.trim();
const clientSecret = config.clientSecret?.trim();
if (!clientId || !clientSecret) throw new Error("OAuth client_id/client_secret required before token exchange");

Type guard

function hasAccessToken(d: unknown): d is { access_token: string } {
  return typeof d === "object" && d !== null && typeof (d as any).access_token === "string" && (d as any).access_token.length > 0;
}

Try / catch

try {
  const token = await exchangeToken(code, verifier, ...);
} catch (err) {
  const msg = err instanceof Error ? err.message : String(err);
  if (msg.startsWith("Token exchange returned no access token")) {
    // surface msg tail (provider error) to user, prompt re-login
  } else throw err;
}

Prevention

When it happens

Trigger: Calling exchangeToken after the OAuth code-for-token exchange when the provider replies 200 with a body missing `access_token`, an empty-string `access_token`, or a Slack-style `{ ok: false, error: ... }` payload.

Common situations: Misconfigured OAuth client (wrong client_secret so the provider returns an error object with 200), authorization code already used/expired, provider-specific non-standard OAuth implementations (Slack-style APIs), or the redirect URL not matching the registered callback.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/845b9192956e5e1a. Report an issue: GitHub.