{"record":{"id":"72c2fb735216fa9e","repo":"HeyPuter/puter","slug":"bad-request-72c2fb","errorCode":"bad_request","errorMessage":"session is required.","messagePattern":"session is required\\.","errorType":"validation","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"src/backend/controllers/auth/AuthController.ts","lineNumber":228,"sourceCode":" * `this.config / this.stores / this.services`, which can't live in a static\n * decorator literal — those are wired imperatively in the `registerRoutes`\n * override below. The override also re-runs the default decorator-walker logic\n * so the rest of the routes register normally.\n */\n@Controller('')\nexport class AuthController extends PuterController {\n    @Post('/login/wait', {\n        subdomain: ['api'],\n        rateLimit: [\n            // 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.\n            { scope: 'login-wait', limit: 100, window: 15 * 60_000, key: 'ip' },\n        ],\n    })\n    async loginWait(req: Request, res: Response) {\n        const { session } = req.body;\n        // validate uuid to prevent ultra long key or listening on pubsub.login.*\n        if (!session || !validateUuid(session)) {\n            throw new HttpError(400, 'session is required.', {\n                legacyCode: 'bad_request',\n            });\n        }\n\n        // Browser-only gate. The session id is client-chosen and travels in a\n        // link, so it is not a secret — the `Origin` header is what actually\n        // says who is asking, and only a browser is prevented from lying about\n        // it. A caller with no `Origin` (curl, a server-side fetch) could\n        // otherwise collect a token minted for someone else's app just by\n        // knowing the id.\n        //\n        // `\"null\"` is rejected too: sandboxed iframes and `file://` documents\n        // serialise their opaque origin that way, and two *unrelated* opaque\n        // origins would compare equal to each other.\n        const reqOrigin = req.headers.origin;\n        if (!reqOrigin || reqOrigin === 'null') {\n            throw new HttpError(403, 'Origin not allowed', {\n                legacyCode: 'forbidden',","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/auth/AuthController.ts#L210-L246","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Generate a fresh UUID (crypto.randomUUID()) for each login attempt and send it as the 'session' body field.","Ensure the same session UUID is used for both /login/wait (poll) and the popup's /login/set (relay).","Confirm the body is JSON and the field is named exactly 'session'."],"exampleFix":"// before\nawait fetch('/login/wait', { method:'POST', body:JSON.stringify({ session: 'abc' }) });\n\n// after\nconst session = crypto.randomUUID();\nawait fetch('/login/wait', { method:'POST', body:JSON.stringify({ session }) });","handlingStrategy":"validation","validationCode":"function validSession(s) {\n  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);\n}\nconst session = crypto.randomUUID();\nif (!validSession(session)) throw new Error('bad session');","typeGuard":"/** @returns {s is string} */\nfunction isUuid(s) {\n  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);\n}","tryCatchPattern":"try { await pollLoginWait(session); }\ncatch (e) { if (e.code === 'bad_request' && /session/.test(e.message)) { /* regenerate UUID */ } else throw e; }","preventionTips":["Always generate the session UUID with crypto.randomUUID().","Reuse the identical UUID across /login/wait and /login/set."],"tags":["auth","login","validation","session","bad-request"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}