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

  1. Inspect the actual 200 body (add logging before this point) to identify the new field name or envelope.
  2. If the upstream renamed the field, update the service to read the new key (e.g. body.access_token).
  3. If the body indicates pending, convert this into a retry/continue-polling path rather than treating 200 as success.
  4. 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

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


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