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

  1. Treat the 408 as 'keep waiting' and re-poll /login/wait (the rate limit allows 100 per 15 min per IP).
  2. Verify the popup is still open and using the same session UUID when it posts to /login/set.
  3. 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

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

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/4a304415440134bf. Report an issue: GitHub.