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
- 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'.
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
- Always generate the session UUID with crypto.randomUUID().
- Reuse the identical UUID across /login/wait and /login/set.
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.