{"record":{"id":"a3a2cbeee84a9e6f","repo":"toeverything/AFFiNE","slug":"invalid-oauth-response","errorCode":"invalid_oauth_response","errorMessage":"Invalid OAuth response: Unable to parse JSON response from ${url}.","messagePattern":"Invalid OAuth response: Unable to parse JSON response from (.+?)\\.","errorType":"exception","errorClass":"InvalidOauthResponse","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/oauth/providers/def.ts","lineNumber":127,"sourceCode":"\n    const body = await response.text();\n    if (!response.ok) {\n      if (response.status < 500 || options?.treatServerErrorAsInvalid) {\n        throw new InvalidOauthCallbackCode({ status: response.status, body });\n      }\n      throw new Error(\n        `Server responded with non-success status ${response.status}, body: ${body}`\n      );\n    }\n\n    if (!body) {\n      return {} as T;\n    }\n\n    try {\n      return JSON.parse(body) as T;\n    } catch {\n      throw new InvalidOauthResponse({\n        reason: `Unable to parse JSON response from ${url}`,\n      });\n    }\n  }\n\n  protected postFormJson<T>(\n    url: string,\n    body: string,\n    options?: {\n      headers?: Record<string, string>;\n      treatServerErrorAsInvalid?: boolean;\n    }\n  ) {\n    return this.fetchJson<T>(\n      url,\n      {\n        method: 'POST',\n        body,","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b6de0ad51b76f3daac2d3d6325369ea623ed7ed4/packages/backend/server/src/plugins/oauth/providers/def.ts#L109-L145","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","For OIDC, take endpoints from the issuer's /.well-known/openid-configuration rather than hand-assembling URLs","Fix reverse-proxy routing so provider API paths are passed through, not rewritten to the frontend"],"exampleFix":"# before: OIDC token endpoint misconfigured\nOIDC_PROVIDER='https://sso.example.com'          # discovery off, endpoints guessed wrong\n\n# after: let discovery resolve real endpoints, or configure exact JSON endpoints\ncurl -s https://sso.example.com/.well-known/openid-configuration | jq .token_endpoint\n# -> https://sso.example.com/realms/affine/protocol/openid-connect/token","handlingStrategy":"try-catch","validationCode":"const endpoints = await (await fetch(`${issuer}/.well-known/openid-configuration`)).json();\nfor (const key of ['token_endpoint', 'userinfo_endpoint', 'jwks_uri']) {\n  const head = await fetch(endpoints[key], { method: 'GET', headers: { Accept: 'application/json' } });\n  const ct = head.headers.get('content-type') ?? '';\n  if (!ct.includes('application/json')) throw new Error(`${key} (${endpoints[key]}) does not serve JSON — got ${ct}`);\n}","typeGuard":"function isInvalidOauthResponse(e: unknown): e is { code: 'invalid_oauth_response'; args: { reason: string } } {\n  return typeof e === 'object' && e !== null && (e as any).code === 'invalid_oauth_response';\n}","tryCatchPattern":"try {\n  await exchangeCode(code, state);\n} catch (e) {\n  if (isInvalidOauthResponse(e) && e.args.reason.includes('Unable to parse JSON')) {\n    // endpoint returned HTML/plain text: log and fix the endpoint/proxy config, do not retry blindly\n    logger.error('OAuth endpoint returned non-JSON', { reason: e.args.reason });\n    return failLoginSetup();\n  }\n  throw e;\n}","preventionTips":["Always derive endpoints from the IdP discovery document instead of hand-building URLs","In reverse-proxy configs, ensure provider API routes pass through untouched — no HTML fallback for API paths","Log the failing URL from the error reason; it names exactly which endpoint betrayed you"],"tags":["oauth","json","http","provider-error","proxy"],"backgroundTag":"invalid-json-response","analyzedSha":"b6de0ad51b76f3daac2d3d6325369ea623ed7ed4","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}