{"record":{"id":"06e75522fbbcdb54","repo":"decolua/9router","slug":"qoder-device-token-poll-invalid-json-response","errorCode":null,"errorMessage":"Qoder device token poll: invalid JSON response (${err.message})","messagePattern":"Qoder device token poll: invalid JSON response \\((.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/qoder.js","lineNumber":132,"sourceCode":"      return { status: \"pending\" };\n    }\n\n    const text = await response.text();\n\n    if (!response.ok) {\n      let message = `Qoder device token poll failed: HTTP ${response.status}`;\n      try {\n        const body = JSON.parse(text);\n        if (body.message) message = `Qoder device token poll failed: ${body.message}`;\n      } catch {}\n      throw new Error(message);\n    }\n\n    let body;\n    try {\n      body = JSON.parse(text);\n    } catch (err) {\n      throw new Error(`Qoder device token poll: invalid JSON response (${err.message})`);\n    }\n\n    // Defensive: 200 + empty token means the upstream changed shape.\n    if (!body.token) {\n      throw new Error(\"Qoder device token poll returned 200 but no token\");\n    }\n\n    const expireMs = QoderService.parseExpiry(body.expires_at, body.expires_in);\n\n    return {\n      status: \"ok\",\n      accessToken: body.token,\n      refreshToken: body.refresh_token || \"\",\n      userId: body.user_id || \"\",\n      expireTime: expireMs,\n      rawResponse: body,\n    };\n  }","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/qoder.js#L114-L150","documentation":"The poll endpoint returned HTTP 200, but the response body could not be parsed as JSON. The service reads the body as text first and JSON.parse's it; on parse failure it throws with the underlying parser message embedded. This signals an upstream contract violation or a non-JSON payload (e.g. an HTML error page served with status 200).","triggerScenarios":"Qoder returns an HTML error/captive-portal page with status 200, an empty body, or a CDN interstitial; proxy middleware rewrites the response body.","commonSituations":"Corporate proxies or VPNs intercepting HTTPS traffic; Qoder changing the endpoint to return plain text; intermittent gateway returning empty 200s during incidents.","solutions":["Log the raw body alongside the error to see what was actually returned (the err.message only shows the parse offset).","Check for transparent proxies / TLS interception that inject HTML into responses.","Retry the poll once — intermittent gateway 200-with-empty-body can self-heal.","If persistent, verify the Qoder endpoint URL/contract hasn't changed and update QODER_DEVICE_TOKEN_URL handling."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const result = await qoder.pollDeviceToken({ nonce, codeVerifier });\n} catch (e) {\n  if (e.message.startsWith('Qoder device token poll: invalid JSON response')) {\n    log.warn('non-JSON 200 from Qoder, likely proxy/interstitial', { err: e.message });\n    if (attempt < maxAttempts) return retryLater();\n  }\n  throw e;\n}","preventionTips":["Check for TLS-intercepting proxies/captive portals in the deployment environment.","Retry once on invalid-JSON 200s; they are often transient gateway artifacts.","Log raw response bodies (via fetch wrapper) when JSON parsing failures recur."],"tags":["oauth","device-flow","json","network"],"backgroundTag":"invalid-json-response","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}