{"record":{"id":"9922385411217054","repo":"decolua/9router","slug":"qoder-pat-exchange-returned-no-job-token","errorCode":null,"errorMessage":"qoder PAT exchange returned no job token","messagePattern":"qoder PAT exchange returned no job token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"open-sse/services/qoderModels.js","lineNumber":88,"sourceCode":"      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 */\nasync function fetchUserIdForJobToken(jobToken, proxyOptions = null, signal = null) {\n  try {\n    const res = await proxyAwareFetch(","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/services/qoderModels.js#L70-L106","documentation":"If the Qoder token-exchange endpoint returns HTTP 200 but the JSON body has no token field, exchangeJobToken throws this error. It indicates the exchange succeeded at transport level but Qoder did not issue a job token — an unexpected/changed response payload.","triggerScenarios":"Qoder returns 200 with JSON lacking data.token (e.g. an error envelope with 200 status, API schema change, or account state that yields no token).","commonSituations":"Qoder API version drift (Cosy-Version header no longer matches a supported client); account in a partially provisioned state; an intermediary returning a 200 HTML/JSON body that parses but has no token.","solutions":["Log the full response body to see what Qoder actually returned","Update/align the QODER_IDE_VERSION and client headers with the currently supported Qoder CLI version","Re-generate the PAT and retry the exchange with a fresh credential","If Qoder changed the field name, update exchangeJobToken in open-sse/services/qoderModels.js to read the new token field"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// after res.ok, narrow the payload before using it\nconst data = await res.json();\nif (!data || typeof data.token !== 'string') {\n  throw new Error('Qoder exchange payload missing token: ' + JSON.stringify(data).slice(0, 200));\n}","typeGuard":"function hasJobToken(d) { return Boolean(d) && typeof d.token === 'string' && d.token.startsWith('jt-'); }","tryCatchPattern":"try {\n  var { jobToken } = await exchangeJobToken(pat, proxyOptions);\n} catch (e) {\n  if (e.message.includes('no job token')) {\n    console.error('Qoder exchange body:', e.message); // capture the actual payload\n    pat = await promptForNewPat();\n    var { jobToken } = await exchangeJobToken(pat, proxyOptions);\n  } else throw e;\n}","preventionTips":["Log unexpected 200 bodies to detect Qoder API schema changes early","Keep QODER_IDE_VERSION / Cosy headers current with the official CLI","Re-generate the PAT if a valid-looking exchange repeatedly returns no token","Report/pin the Qoder endpoint version in config so upgrades are deliberate"],"tags":["qoder","response-parsing","schema"],"backgroundTag":"schema-validation-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}