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

  1. Verify the provider's TokenURL in email OAuth config is the real token issuance endpoint (ends in /token, not /authorize)
  2. 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
  3. Confirm client_id/client_secret/redirect_uri and scopes are accepted by the provider
  4. 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

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


AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15). Data as JSON: /api/errors/b5bb2333c13b0fa3. Report an issue: GitHub.