HeyPuter/puter · error · HttpError

bad_request

bad_request

Error message

session is required.

What it means

Returned by POST /login/wait when the 'session' field is missing or fails validateUuid. The session id is client-chosen and used to build the pubsub login channel key, so it must be a well-formed UUID — this also prevents ultra-long keys or listening on broad pubsub.login.* channels. It is a request-shape validation error.

Source

Thrown at src/backend/controllers/auth/AuthController.ts:228

 * `this.config / this.stores / this.services`, which can't live in a static
 * decorator literal — those are wired imperatively in the `registerRoutes`
 * override below. The override also re-runs the default decorator-walker logic
 * so the rest of the routes register normally.
 */
@Controller('')
export class AuthController extends PuterController {
    @Post('/login/wait', {
        subdomain: ['api'],
        rateLimit: [
            // A client will make a request to this every 10 seconds while waiting for the login to complete, so we allow a higher limit than the main /login endpoint.
            { scope: 'login-wait', limit: 100, window: 15 * 60_000, key: 'ip' },
        ],
    })
    async loginWait(req: Request, res: Response) {
        const { session } = req.body;
        // validate uuid to prevent ultra long key or listening on pubsub.login.*
        if (!session || !validateUuid(session)) {
            throw new HttpError(400, 'session is required.', {
                legacyCode: 'bad_request',
            });
        }

        // Browser-only gate. The session id is client-chosen and travels in a
        // link, so it is not a secret — the `Origin` header is what actually
        // says who is asking, and only a browser is prevented from lying about
        // it. A caller with no `Origin` (curl, a server-side fetch) could
        // otherwise collect a token minted for someone else's app just by
        // knowing the id.
        //
        // `"null"` is rejected too: sandboxed iframes and `file://` documents
        // serialise their opaque origin that way, and two *unrelated* opaque
        // origins would compare equal to each other.
        const reqOrigin = req.headers.origin;
        if (!reqOrigin || reqOrigin === 'null') {
            throw new HttpError(403, 'Origin not allowed', {
                legacyCode: 'forbidden',

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Generate a fresh UUID (crypto.randomUUID()) for each login attempt and send it as the 'session' body field.
  2. Ensure the same session UUID is used for both /login/wait (poll) and the popup's /login/set (relay).
  3. Confirm the body is JSON and the field is named exactly 'session'.

Example fix

// before
await fetch('/login/wait', { method:'POST', body:JSON.stringify({ session: 'abc' }) });

// after
const session = crypto.randomUUID();
await fetch('/login/wait', { method:'POST', body:JSON.stringify({ session }) });
Defensive patterns

Strategy: validation

Validate before calling

function validSession(s) {
  return typeof s === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
}
const session = crypto.randomUUID();
if (!validSession(session)) throw new Error('bad session');

Type guard

/** @returns {s is string} */
function isUuid(s) {
  return typeof s === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
}

Try / catch

try { await pollLoginWait(session); }
catch (e) { if (e.code === 'bad_request' && /session/.test(e.message)) { /* regenerate UUID */ } else throw e; }

Prevention

When it happens

Trigger: Calling /login/wait with no session in the body, with a non-UUID string, or with an empty value. The id must be a v4-style UUID generated by the client before starting the popup login flow.

Common situations: Client forgot to generate/marshal the session UUID before polling; passing an internal incrementing id instead of a UUID; integration code that sends the wrong field name.

Related errors


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