HeyPuter/puter · error · HttpError

token_missing

token_missing

Error message

Missing `token`

What it means

Thrown by the WISP relay-token verify endpoint (POST /wisp/relay-token/verify on the api.* subdomain) when req.body.token is falsy or not a string. It is the input gate before the token is JWT-verified by TokenService. Returns 400 with legacyCode token_missing.

Source

Thrown at src/backend/controllers/wisp/WispController.ts:110

        } else {
            const token = this.services.token.sign(
                'wisp',
                {
                    $: 'token:wisp',
                    $v: '0.0.0',
                    guest: true,
                },
                { expiresIn: '1d' },
            );
            res.json({ token, server: wispCfg.server ?? null });
        }
    };

    /** POST /wisp/relay-token/verify — verify a relay token and apply policy. */
    #verify = async (req: Request, res: Response): Promise<void> => {
        const bodyToken = req.body?.token;
        if (!bodyToken || typeof bodyToken !== 'string') {
            throw new HttpError(400, 'Missing `token`', {
                legacyCode: 'token_missing',
            });
        }

        let decoded: Record<string, unknown>;
        try {
            decoded = this.services.token.verify<Record<string, unknown>>(
                'wisp',
                bodyToken,
            );
            if (decoded.$ !== 'token:wisp')
                throw new HttpError(403, 'wrong token type', {
                    legacyCode: 'invalid_token',
                });
        } catch {
            throw new HttpError(403, 'Forbidden', {
                legacyCode: 'invalid_token',
            });

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Send a JSON body { "token": "<jwt-string>" } with Content-Type: application/json.
  2. Obtain the token first from POST /wisp/relay-token/create and pass its response.token into verify.
  3. Confirm the token variable is defined and is a string before the request (guard against undefined).
  4. Verify the HTTP client is sending the body as JSON, not form-encoded.

Example fix

// before
await fetch('/wisp/relay-token/verify', {
  method: 'POST',
  body: JSON.stringify({}), // token omitted
});

// after
const { token } = await (await fetch('/wisp/relay-token/create', { method: 'POST' })).json();
await fetch('/wisp/relay-token/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token }),
});
Defensive patterns

Strategy: validation

Validate before calling

function validTokenBody(body) {
  return body != null && typeof body.token === 'string' && body.token.length > 0;
}
if (!validTokenBody(req.body)) {
  throw new Error('Request body must include a string `token`');
}

Type guard

function isTokenBody(body) {
  return body != null && typeof body === 'object' && typeof body.token === 'string';
}

Try / catch

try {
  await fetch('/wisp/relay-token/verify', { method: 'POST', body: JSON.stringify({ token }) });
} catch (e) {
  if (e.status === 400 && /token/.test(e.message)) {
    const created = await fetch('/wisp/relay-token/create', { method: 'POST' });
    token = (await created.json()).token;
    await fetch('/wisp/relay-token/verify', { method: 'POST', body: JSON.stringify({ token }) });
  } else throw e;
}

Prevention

When it happens

Trigger: A POST to /wisp/relay-token/verify whose JSON body omits the token field, sets it to null/empty, sends a non-string type (number/object), or whose body parser failed to populate req.body (wrong Content-Type).

Common situations: Client forgot to include the token in the JSON body; sent it as a query param or header instead of the body; wrong Content-Type (not application/json) so Express did not parse the body; token variable undefined due to a prior step failing; rate limit (300/min per IP) masking the real cause.

Related errors


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