{"record":{"id":"e519f4da881582ff","repo":"decolua/9router","slug":"qoder-pat-exchange-failed-res-status-text-sl","errorCode":null,"errorMessage":"qoder PAT exchange failed: ${res.status} ${text.slice(0, 200)}","messagePattern":"qoder PAT exchange failed: (.+?) (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"open-sse/services/qoderModels.js","lineNumber":85,"sourceCode":"  const res = await proxyAwareFetch(\n    QODER_JOB_TOKEN_EXCHANGE_URL,\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Accept: \"application/json\",\n        \"User-Agent\": \"qodercli/1.0.0\",\n        \"Cosy-Version\": QODER_IDE_VERSION,\n        \"Cosy-ClientType\": QODER_CLIENT_TYPE,\n      },\n      body: JSON.stringify({ personal_token: pat }),\n      signal,\n    },\n    proxyOptions,\n  );\n  if (!res.ok) {\n    const text = await res.text().catch(() => \"\");\n    throw new Error(`qoder PAT exchange failed: ${res.status} ${text.slice(0, 200)}`);\n  }\n  const data = await res.json();\n  if (!data.token) throw new Error(\"qoder PAT exchange returned no job token\");\n\n  let expiresAt = Date.now() + PAT_DEFAULT_TTL_MS;\n  if (data.expires_at) {\n    const parsed = Date.parse(data.expires_at);\n    if (!Number.isNaN(parsed)) expiresAt = parsed;\n  } else if (typeof data.expires_in === \"number\" && data.expires_in > 0) {\n    expiresAt = Date.now() + data.expires_in;\n  }\n  return { jobToken: data.token, jobRefreshToken: data.refresh_token || \"\", expiresAt };\n}\n\n/**\n * Resolve the Qoder userId for a job token (needed for COSY signing).\n * Returns \"\" on any failure — callers fall back to the stored userId.\n */","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/services/qoderModels.js#L67-L103","documentation":"exchangeJobToken swaps a Qoder personal access token (pt-...) for a short-lived job token (jt-...) via a plain JSON POST. On a non-OK response it throws with the HTTP status and first 200 chars of the body. This means Qoder's token-exchange endpoint rejected the request or the PAT.","triggerScenarios":"The POST to Qoder's job-token exchange URL returns non-2xx — 401 for an invalid/revoked/misspelled PAT, 403 for a disabled account, 429 rate limit, or 5xx from Qoder.","commonSituations":"User pasted an expired or wrong-type token (not a pt- token); PAT revoked in the Qoder dashboard; corporate proxy intercepting the exchange; Qoder API outage.","solutions":["Verify the stored Qoder credential is a valid, unexpired PAT starting with pt- and re-generate it in the Qoder dashboard if in doubt","Check the response body embedded in the message (200 chars) for the precise Qoder error reason","If 429, back off and retry after the rate-limit window","Confirm proxy options are correct and the endpoint is reachable from your network"],"exampleFix":"// before\nconst { jobToken } = await exchangeJobToken(pat);\n// after\ntry {\n  var { jobToken } = await exchangeJobToken(pat);\n} catch (e) {\n  if (/exchange failed: 401/.test(e.message)) {\n    pat = await promptUserForNewPat();\n    var { jobToken } = await exchangeJobToken(pat);\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// validate the credential shape before exchanging\nif (typeof pat !== 'string' || !pat.startsWith('pt-')) {\n  throw new Error('Store a valid Qoder personal access token (pt-...) first.');\n}","typeGuard":"function isQoderPat(t) { return typeof t === 'string' && /^pt-[A-Za-z0-9_-]+$/.test(t); }","tryCatchPattern":"try {\n  const { jobToken } = await exchangeJobToken(pat, proxyOptions);\n} catch (e) {\n  if (/exchange failed: 401/.test(e.message)) {\n    pat = await promptForNewPat();          // re-create PAT in Qoder dashboard\n    return exchangeJobToken(pat, proxyOptions);\n  }\n  if (/exchange failed: 429/.test(e.message)) { await backoff(); return exchangeJobToken(pat, proxyOptions); }\n  throw e;\n}","preventionTips":["Regenerate Qoder PATs before their expiry and update the stored credential","Keep the Cosy-Version / client headers aligned with the supported Qoder CLI version","Read the 200-char body in the message — it states why Qoder rejected the exchange","Test the exchange once at connection setup so a bad PAT fails early, not mid-session"],"tags":["network","http","auth","qoder"],"backgroundTag":"http-error-response","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}