HeyPuter/puter · warning · HttpError
request_timeout
request_timeout
Error message
Request timeout.
What it means
Returned by POST /login/wait when no auth token arrived on the pubsub.login.{session} channel within the 10-second wait window. The poller is expected to call /login/set from the popup after sign-in; if that never happens (or happens too slowly), the long-poll times out. Clients are designed to retry this endpoint periodically while waiting.
Source
Thrown at src/backend/controllers/auth/AuthController.ts:272
const expectedAppUid =
await this.services.auth.appUidFromOrigin(reqOrigin);
const { resolve, promise } = Promise.withResolvers<void>();
let token: string | null = null;
const listener = (_key: string, value: { authtoken: string }) => {
token = value.authtoken;
resolve();
};
this.clients.event.on(`pubsub.login.${session}`, listener);
const timeout = new Promise<void>((resolve) =>
setTimeout(resolve, 10000),
);
await Promise.race([promise, timeout]);
this.clients.event.off(`pubsub.login.${session}`, listener);
if (!token) {
throw new HttpError(408, 'Request timeout.', {
legacyCode: 'request_timeout',
});
}
// Audience check. The postMessage hand-off this relay stands in for
// is origin-bound for free — it posts with `targetOrigin`, so a page
// can only ever receive a token minted for *itself*. Delivering
// server-side dropped that binding; this restores it. Without it a
// popup talked into minting for app X (see `trustsOpenerOriginParam`
// in the GUI) hands X's token to whoever holds the session id.
if (!this.#tokenIsForApp(token, expectedAppUid)) {
// Deliberately the same 408 the no-token path returns: a caller
// learns only that nothing arrived for them, not that a token
// for a different app went past.
throw new HttpError(408, 'Request timeout.', {
legacyCode: 'request_timeout',
});
}View on GitHub (pinned to 908ec23eda)
Solutions
- Treat the 408 as 'keep waiting' and re-poll /login/wait (the rate limit allows 100 per 15 min per IP).
- Verify the popup is still open and using the same session UUID when it posts to /login/set.
- If the user closed the popup, stop polling and surface a login-cancelled UI.
Example fix
// before: single poll gives up on timeout
try { await fetch('/login/wait', {...}); } catch { throw new Error('login failed'); }
// after: retry until the user cancels
while (!cancelled) {
try { const r = await fetch('/login/wait', {...}); if (r.ok) return await r.json(); }
catch (e) { if (e.code !== 'request_timeout') throw e; }
} Defensive patterns
Strategy: retry
Try / catch
while (!cancelled) {
try {
const r = await fetch('/login/wait', { method:'POST', body:JSON.stringify({ session }) });
if (r.ok) return await r.json();
} catch (e) {
if (e.code !== 'request_timeout') throw e;
}
} Prevention
- Treat 408 as 'keep polling' — the rate limit budgets 100 polls per 15 min.
- Stop polling when the user closes the popup.
When it happens
Trigger: The user has not yet completed sign-in in the popup; the popup's /login/set call failed or was never made; network/event-bus latency exceeded 10s; the session id used by /login/wait differs from the one used by /login/set.
Common situations: Normal user is still typing credentials; popup was closed before completing login; mismatched session UUIDs between the polling page and the popup; event bus disruption.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/4a304415440134bf.
Report an issue: GitHub.