toeverything/AFFiNE · error · InvalidOauthResponse

invalid_oauth_response

invalid_oauth_response

Error message

Invalid OAuth response: Unable to parse JSON response from ${url}.

What it means

fetchJson got a 2xx response whose body is not valid JSON (empty bodies short-circuit to {}, so this is genuinely unparseable text). The provider URL answered successfully but with HTML or plain text — typically a login/error page, a rate-limit notice, or a proxy interstitial — so the OAuth client cannot deserialize it and throws InvalidOauthResponse naming the URL.

Source

Thrown at packages/backend/server/src/plugins/oauth/providers/def.ts:127

    const body = await response.text();
    if (!response.ok) {
      if (response.status < 500 || options?.treatServerErrorAsInvalid) {
        throw new InvalidOauthCallbackCode({ status: response.status, body });
      }
      throw new Error(
        `Server responded with non-success status ${response.status}, body: ${body}`
      );
    }

    if (!body) {
      return {} as T;
    }

    try {
      return JSON.parse(body) as T;
    } catch {
      throw new InvalidOauthResponse({
        reason: `Unable to parse JSON response from ${url}`,
      });
    }
  }

  protected postFormJson<T>(
    url: string,
    body: string,
    options?: {
      headers?: Record<string, string>;
      treatServerErrorAsInvalid?: boolean;
    }
  ) {
    return this.fetchJson<T>(
      url,
      {
        method: 'POST',
        body,

View on GitHub (pinned to b6de0ad51b)

Solutions

  1. Log the URL in the error — fetch that exact URL with curl and inspect what comes back; if it's HTML, the endpoint is wrong or intercepted
  2. For OIDC, take endpoints from the issuer's /.well-known/openid-configuration rather than hand-assembling URLs
  3. Fix reverse-proxy routing so provider API paths are passed through, not rewritten to the frontend

Example fix

# before: OIDC token endpoint misconfigured
OIDC_PROVIDER='https://sso.example.com'          # discovery off, endpoints guessed wrong

# after: let discovery resolve real endpoints, or configure exact JSON endpoints
curl -s https://sso.example.com/.well-known/openid-configuration | jq .token_endpoint
# -> https://sso.example.com/realms/affine/protocol/openid-connect/token
Defensive patterns

Strategy: try-catch

Validate before calling

const endpoints = await (await fetch(`${issuer}/.well-known/openid-configuration`)).json();
for (const key of ['token_endpoint', 'userinfo_endpoint', 'jwks_uri']) {
  const head = await fetch(endpoints[key], { method: 'GET', headers: { Accept: 'application/json' } });
  const ct = head.headers.get('content-type') ?? '';
  if (!ct.includes('application/json')) throw new Error(`${key} (${endpoints[key]}) does not serve JSON — got ${ct}`);
}

Type guard

function isInvalidOauthResponse(e: unknown): e is { code: 'invalid_oauth_response'; args: { reason: string } } {
  return typeof e === 'object' && e !== null && (e as any).code === 'invalid_oauth_response';
}

Try / catch

try {
  await exchangeCode(code, state);
} catch (e) {
  if (isInvalidOauthResponse(e) && e.args.reason.includes('Unable to parse JSON')) {
    // endpoint returned HTML/plain text: log and fix the endpoint/proxy config, do not retry blindly
    logger.error('OAuth endpoint returned non-JSON', { reason: e.args.reason });
    return failLoginSetup();
  }
  throw e;
}

Prevention

When it happens

Trigger: An OIDC endpoint URL misconfigured to an HTML page (using the issuer root instead of the token_endpoint, or discovery doc not actually OIDC); corporate proxy or captive portal injecting an HTML response; provider serving a maintenance/rate-limit page with status 200.

Common situations: OIDC_PROVIDER base URL with a typo; pointing token/userinfo endpoints at a path that returns the SPA index.html; reverse proxy (nginx) returning the frontend app for API paths; CDN rate-limiting with an HTML block page.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b6de0ad51b (2026-08-18). Data as JSON: /api/errors/a3a2cbeee84a9e6f. Report an issue: GitHub.