decolua/9router · error · Error

qoder PAT exchange failed: ${res.status} ${text.slice(0, 200

Error message

qoder PAT exchange failed: ${res.status} ${text.slice(0, 200)}

What it means

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.

Source

Thrown at open-sse/services/qoderModels.js:85

  const res = await proxyAwareFetch(
    QODER_JOB_TOKEN_EXCHANGE_URL,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        "User-Agent": "qodercli/1.0.0",
        "Cosy-Version": QODER_IDE_VERSION,
        "Cosy-ClientType": QODER_CLIENT_TYPE,
      },
      body: JSON.stringify({ personal_token: pat }),
      signal,
    },
    proxyOptions,
  );
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`qoder PAT exchange failed: ${res.status} ${text.slice(0, 200)}`);
  }
  const data = await res.json();
  if (!data.token) throw new Error("qoder PAT exchange returned no job token");

  let expiresAt = Date.now() + PAT_DEFAULT_TTL_MS;
  if (data.expires_at) {
    const parsed = Date.parse(data.expires_at);
    if (!Number.isNaN(parsed)) expiresAt = parsed;
  } else if (typeof data.expires_in === "number" && data.expires_in > 0) {
    expiresAt = Date.now() + data.expires_in;
  }
  return { jobToken: data.token, jobRefreshToken: data.refresh_token || "", expiresAt };
}

/**
 * Resolve the Qoder userId for a job token (needed for COSY signing).
 * Returns "" on any failure — callers fall back to the stored userId.
 */

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Verify the stored Qoder credential is a valid, unexpired PAT starting with pt- and re-generate it in the Qoder dashboard if in doubt
  2. Check the response body embedded in the message (200 chars) for the precise Qoder error reason
  3. If 429, back off and retry after the rate-limit window
  4. Confirm proxy options are correct and the endpoint is reachable from your network

Example fix

// before
const { jobToken } = await exchangeJobToken(pat);
// after
try {
  var { jobToken } = await exchangeJobToken(pat);
} catch (e) {
  if (/exchange failed: 401/.test(e.message)) {
    pat = await promptUserForNewPat();
    var { jobToken } = await exchangeJobToken(pat);
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// validate the credential shape before exchanging
if (typeof pat !== 'string' || !pat.startsWith('pt-')) {
  throw new Error('Store a valid Qoder personal access token (pt-...) first.');
}

Type guard

function isQoderPat(t) { return typeof t === 'string' && /^pt-[A-Za-z0-9_-]+$/.test(t); }

Try / catch

try {
  const { jobToken } = await exchangeJobToken(pat, proxyOptions);
} catch (e) {
  if (/exchange failed: 401/.test(e.message)) {
    pat = await promptForNewPat();          // re-create PAT in Qoder dashboard
    return exchangeJobToken(pat, proxyOptions);
  }
  if (/exchange failed: 429/.test(e.message)) { await backoff(); return exchangeJobToken(pat, proxyOptions); }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/e519f4da881582ff. Report an issue: GitHub.