pbakaus/impeccable · error · Error

Poll failed: ${res.status} ${res.statusText}

Error message

Poll failed: ${res.status} ${res.statusText}

What it means

Thrown by fetchNextEvent() when the GET /poll long-poll request returns a non-ok, non-401 status. Distinct from error 55 because /poll is the long-poll endpoint (with timeout/leaseMs/types query params). Like /status, 401 maps to AUTH_FAILED; any other status becomes 'Poll failed: <code> <reason>'. The function loops on 'timeout' responses, so this throw breaks the loop.

Source

Thrown at plugin/skills/impeccable/scripts/live-poll.mjs:193

      : PER_REQUEST_TIMEOUT_MS;
    const slice = Math.min(Math.max(remaining, 1000), perRequestTimeoutMs);
    const query = new URLSearchParams({
      token,
      timeout: String(slice),
      leaseMs: String(leaseMs),
    });
    const normalizedTypes = normalizePollTypes(resolveTypes ? await resolveTypes() : types);
    if (normalizedTypes.length > 0) query.set('types', normalizedTypes.join(','));
    const res = await fetch(`${base}/poll?${query}`, { signal });

    if (res.status === 401) {
      const err = new Error('Authentication failed. The server token may have changed.');
      err.code = 'AUTH_FAILED';
      throw err;
    }

    if (!res.ok) {
      throw new Error(`Poll failed: ${res.status} ${res.statusText}`);
    }

    const next = await res.json();
    if (next?.type === 'timeout') {
      if (totalDeadline && Date.now() < totalDeadline) continue;
      if (!totalDeadline) continue;
      return next;
    }
    return next;
  }
}

export async function augmentEventWithAcceptHandling(event, base, token) {
  if (event.type !== 'accept' && event.type !== 'discard') return event;

  const __dirname = path.dirname(fileURLToPath(import.meta.url));
  const acceptScript = path.join(__dirname, 'live-accept.mjs');
  const scriptArgs = buildAcceptScriptArgs(event);

View on GitHub (pinned to d14711ae3d)

Solutions

  1. Restart the live server (stop via server.json pid, then `live.mjs`) and resume polling.
  2. Check server logs for the 5xx root cause — poll errors usually mirror an internal exception.
  3. Verify client and server are from the same skill version (the /poll contract includes token, timeout, leaseMs, types).
  4. If ECONNREFUSED, the server isn't running — that's surfaced separately by handlePollError, but a stale server.json can mislead.

Example fix

// before
const event = await fetchNextEvent(base, token, { types });

// after
try {
  const event = await fetchNextEvent(base, token, { types });
} catch (err) {
  if (err.code === 'AUTH_FAILED') { /* re-auth */ }
  else if (/Poll failed:/.test(err.message)) {
    console.error('Poll endpoint error, restart live session:', err.message);
  }
  throw err;
}
Defensive patterns

Strategy: retry

Try / catch

async function safePoll(base, token, opts, retries = 1) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fetchNextEvent(base, token, opts);
    } catch (err) {
      if (err.code === 'AUTH_FAILED') throw err;
      if (attempt < retries && /Poll failed: 5\d\d/.test(err.message)) {
        await new Promise((r) => setTimeout(r, 500));
        continue;
      }
      throw err;
    }
  }
}

Prevention

When it happens

Trigger: Server returns 500 due to an internal error mid-lease; 404 if the /poll route is missing (version skew); 429 if the server rate-limits; 503 during shutdown. The poll uses a perRequestTimeoutMs capped at 270s (PER_REQUEST_TIMEOUT_MS) to stay under Node fetch's 300s header timeout.

Common situations: Server crashed under load; polling after the server was stopped but before the port was released to another process; version mismatch where the client sends query params the server doesn't understand; lease corruption causing the server to error on event delivery.

Related errors


AI-assisted analysis of pbakaus/impeccable@d14711ae3d (2026-08-13). Data as JSON: /api/errors/efd97c4f9f5be968. Report an issue: GitHub.