decolua/9router · error
Qoder device token poll returned 200 but no token
Error message
Qoder device token poll returned 200 but no token
What it means
A defensive check: the poll returned HTTP 200 with parseable JSON, but the body has no truthy 'token' field. The service treats this as proof the upstream response shape changed (or a placeholder success was returned) and refuses to return a useless result. It's terminal — the caller never receives a token object.
Source
Thrown at src/lib/oauth/services/qoder.js:137
if (!response.ok) {
let message = `Qoder device token poll failed: HTTP ${response.status}`;
try {
const body = JSON.parse(text);
if (body.message) message = `Qoder device token poll failed: ${body.message}`;
} catch {}
throw new Error(message);
}
let body;
try {
body = JSON.parse(text);
} catch (err) {
throw new Error(`Qoder device token poll: invalid JSON response (${err.message})`);
}
// Defensive: 200 + empty token means the upstream changed shape.
if (!body.token) {
throw new Error("Qoder device token poll returned 200 but no token");
}
const expireMs = QoderService.parseExpiry(body.expires_at, body.expires_in);
return {
status: "ok",
accessToken: body.token,
refreshToken: body.refresh_token || "",
userId: body.user_id || "",
expireTime: expireMs,
rawResponse: body,
};
}
/**
* Fetch profile info for the freshly-issued token. Best-effort — failures
* shouldn't block login; returning empty strings is fine.
*/View on GitHub (pinned to 90b52e06ff)
Solutions
- Inspect the actual 200 body (add logging before this point) to identify the new field name or envelope.
- If the upstream renamed the field, update the service to read the new key (e.g. body.access_token).
- If the body indicates pending, convert this into a retry/continue-polling path rather than treating 200 as success.
- Pin/verify the Qoder API version your deployment targets.
Example fix
// before
if (!body.token) throw new Error("Qoder device token poll returned 200 but no token");
// after
const token = body.token ?? body.access_token ?? body.data?.token;
if (!token) throw new Error("Qoder device token poll returned 200 but no token"); Defensive patterns
Strategy: try-catch
Type guard
function isTokenResponse(body) {
return body !== null && typeof body === 'object' &&
typeof body.token === 'string' && body.token.length > 0;
} Try / catch
try {
const result = await qoder.pollDeviceToken({ nonce, codeVerifier });
if (!isTokenResponse(result)) throw new Error('upstream shape changed: inspect body');
} catch (e) {
if (/200 but no token/.test(e.message)) {
log.error('Qoder contract drift', e.message); // capture payload upstream for diagnosis
throw e;
}
throw e;
} Prevention
- Log the full 200 body when this fires to identify the renamed/nested token field.
- Add an integration test asserting the current response shape to catch upstream drift early.
- Keep the service's field-reading logic tolerant of common aliases (access_token, data.token).
When it happens
Trigger: Qoder returns 200 with { status: 'pending' }-style JSON, an empty object, or a renamed field (access_token instead of token) after an upstream API change.
Common situations: Qoder deploying an API revision that renames or nests the token field; poll hitting a stub/mock endpoint in a test environment; response wrapped in an envelope like { data: { token } }.
Related errors
- `CodeBuddy state request failed: ${await response.text()}`
- `CodeBuddy state error: ${data.msg || "missing state/authUrl
- `CodeBuddy Intl state request failed: ${await response.text(
- `CodeBuddy Intl state error: ${data.msg || "missing state/au
- `Device code request failed: ${error}`
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/419599c026c43742.
Report an issue: GitHub.