Wei-Shaw/sub2api · error
token_exchange_failed
token_exchange_failed
Error message
missing access_token
What it means
Thrown after a successful HTTP 2xx response from the OAuth token endpoint when the parsed JSON body has an empty/absent access_token field (code=token_exchange_failed). The endpoint was reached and returned success, but the payload is not a valid token response. Transport errors and non-2xx statuses are separate, earlier failures.
Source
Thrown at backend/internal/handler/auth_email_oauth.go:495
"grant_type": "authorization_code",
"client_id": cfg.ClientID,
"client_secret": cfg.ClientSecret,
"code": code,
"redirect_uri": cfg.RedirectURL,
}).
Post(cfg.TokenURL)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("token endpoint status %d: %s", resp.StatusCode, truncateLogValue(resp.String(), 1024))
}
var tokenResp emailOAuthTokenResponse
if err := json.Unmarshal(resp.Bytes(), &tokenResp); err != nil {
return nil, err
}
if strings.TrimSpace(tokenResp.AccessToken) == "" {
return nil, errors.New("missing access_token")
}
return &tokenResp, nil
}
func fetchEmailOAuthProfile(ctx context.Context, provider string, cfg config.EmailOAuthProviderConfig, token *emailOAuthTokenResponse) (*emailOAuthProfile, error) {
resp, err := req.C().
R().
SetContext(ctx).
SetBearerAuthToken(token.AccessToken).
SetHeader("Accept", "application/json").
Get(cfg.UserInfoURL)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("userinfo endpoint status %d: %s", resp.StatusCode, truncateLogValue(resp.String(), 1024))
}
switch strings.ToLower(strings.TrimSpace(provider)) {View on GitHub (pinned to 073e92d171)
Solutions
- Verify the provider's TokenURL in email OAuth config is the real token issuance endpoint (ends in /token, not /authorize)
- Log/inspect the raw token endpoint response body (server side it is truncated to 1024 chars in the status-code error path) to see what was actually returned
- Confirm client_id/client_secret/redirect_uri and scopes are accepted by the provider
- Check for corporate proxies or middleware that rewrite responses
Example fix
// before (config) TokenURL: "https://provider.com/o/oauth2/auth" // after TokenURL: "https://provider.com/o/oauth2/token"
Defensive patterns
Strategy: validation
Validate before calling
// TS client: pre-flight check that the token endpoint is plausible
function assertTokenURL(u: string) {
const p = new URL(u);
if (!p.pathname.endsWith('/token')) throw new Error(`TokenURL looks wrong: ${u} (expected a /token endpoint)`);
} Try / catch
// Server-side Go: distinguish shape errors from transport errors
if _, err := exchangeToken(ctx, cfg); err != nil {
if err.Error() == "missing access_token" {
// log raw body (already truncated upstream) and surface a config-hint error
log.Warn("token endpoint returned no access_token; verify TokenURL", zap.String("url", cfg.TokenURL))
}
return err
} Prevention
- Smoke-test the token endpoint with curl before enabling the provider
- Assert token URLs end in /token in config validation at startup
- Never point TokenURL at an authorize or userinfo endpoint
When it happens
Trigger: Email OAuth login where the configured TokenURL returns 200 with an error body, an HTML login page, or a JSON structure using a different field name (e.g. missing access_token).
Common situations: Wrong TokenURL (pointing at an authorize or userinfo endpoint instead of the token endpoint); a proxy or captive portal intercepting the request; provider API changes; scopes/redirect_uri rejected with a 200 error envelope.
Related errors
- dingtalk: internal_only requires app_type=internal
- userinfo_failed
- base URL must not include a query
- base URL must not include a fragment
- auth.dingtalk.callbackMissingToken
AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15).
Data as JSON: /api/errors/b5bb2333c13b0fa3.
Report an issue: GitHub.