pbakaus/impeccable · error · Error
Poll failed
Error message
Poll failed: ${res.status} ${res.statusText} What it means
Inside fetchNextEvent() (skill/scripts/live-poll.mjs), each long-poll round issues GET ${base}/poll?token=...&timeout=...&types=...&lease=... . A 401 is first converted to AUTH_FAILED; any other non-OK response throws 'Poll failed: <status> <statusText>', aborting the poll loop. Note the protocol itself returns HTTP 200 with a {type:'timeout'} JSON body for normal idle expiry — so this error always means an abnormal HTTP-level failure, never a routine poll timeout.
Solutions
- Check the live-server console — 5xx statuses almost always have a matching stack trace there
- If a proxy fronts the server, raise its proxy_read_timeout above the poll slice so idle long polls are not cut into 504s
- Verify base and token still match the running server (a mismatch shows up as 401/AUTH_FAILED, which is handled separately)
- Retry fetchNextEvent with backoff — a transient 5xx during a server reload or while it is busy scaffolding is expected to clear
Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight the poll endpoint once before entering the long-poll loop:
const probe = await fetch(`${base}/poll?token=${token}&timeout=1000`);
if (probe.status === 401) throw new Error('token mismatch — re-read server token');
if (!probe.ok) throw new Error(`poll endpoint unhealthy: HTTP ${probe.status}`); Try / catch
while (running) {
try {
event = await fetchNextEvent(base, token, { signal });
} catch (err) {
if (err?.code === 'AUTH_FAILED') throw err; // stop: fix the token
if (!/^Poll failed: 5\d\d/.test(err?.message || '')) throw err;
await backoff(); // transient 5xx: retry the loop
continue;
}
} Prevention
- Set proxy read timeouts above the poll slice when a proxy fronts the live server — idle long polls are by design, not hangs
- Watch the live-server console for 5xx root causes instead of retrying blindly
- Treat 401/AUTH_FAILED as a distinct terminal case: the server token changed under you
When it happens
Trigger: The live server returns 500 from /poll (server-side exception); a reverse proxy in front times out the intentionally idle long-poll connection and answers 504/502; the server restarted mid-poll and the next request lands on a replacement process that errors; base/port drift so a foreign service answers.
Common situations: nginx/Caddy or dev proxies with read timeouts shorter than the poll slice (perRequestTimeoutMs) killing idle upgrades; server crashes during heavy operations; mixed old client against newer server where query parameters are rejected.
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
- Status failed
- Could not check for updates
- Could not fetch command list from impeccable.style. Check…
- Live server not running. Start one with
- Live server not running. Start one with
AI-assisted analysis of pbakaus/impeccable@f88b2837a7 (2026-08-18).
Data as JSON: /api/errors/71572829245c0089.
Report an issue: GitHub.
Appendix: source
Thrown at skill/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 f88b2837a7)