HeyPuter/puter · critical · HttpError

internal_error

internal_error

Error message

user-protected state missing

What it means

HTTP 500 raised in the `verifyIdentity` step of userProtected when `req.userProtected.user` is unset. That value is populated exclusively by the preceding `refreshUser` middleware, so a 500 here means the gate array was mis-wired: verifyIdentity ran without refreshUser, or someone selected a subset of the returned array. This is an internal backend bug, never a legitimate client error.

Source

Thrown at src/backend/core/http/middleware/userProtected.ts:200

    // 3. Password (bcrypt) OR valid OIDC revalidation cookie.
    //
    //   - Temp users (no password + no email) pass only when the route was
    //     registered with `allowTempUsers: true` (delete-own-user).
    //   - `req.body.password` → bcrypt match against user row. OIDC-only
    //     accounts bounce with `oidc_revalidation_required` + a
    //     `revalidate_url` helper so the GUI can open the OIDC popup.
    //   - Otherwise accept a valid `puter_revalidation` cookie. Expiry,
    //     `purpose === 'revalidate'`, matching `user_uuid` all required.
    //   - Password account, neither credential → 403 `password_required`.
    const verifyIdentity: RequestHandler = async (
        req: Request,
        _res: Response,
        next: NextFunction,
    ) => {
        const user = req.userProtected?.user;
        if (!user)
            throw new HttpError(500, 'user-protected state missing', {
                legacyCode: 'internal_error',
            });

        const isTemp = user.password === null && user.email === null;
        if (isTemp) {
            if (allowTemp) return next();
            throw new HttpError(403, 'Temporary account', {
                legacyCode: 'temporary_account',
            });
        }

        const bodyPassword =
            typeof req.body?.password === 'string' ? req.body.password : null;
        if (bodyPassword) {
            if (user.password === null) {
                const fields = await buildRevalidateFields(
                    config,
                    oidcService,

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Register the entire array returned by createUserProtectedGate — do not pick subsets.
  2. Ensure refreshUser runs before verifyIdentity (it sets req.userProtected).
  3. If using a custom chain, add the refreshUser step that populates req.userProtected.user.
  4. Report as a backend bug if the array is intact.

Example fix

// before
app.delete('/user', verifyIdentity, handler); // missing refreshUser
// after
const gate = createUserProtectedGate(deps, { allowTempUsers: true });
app.delete('/user', ...gate, handler); // full chain
Defensive patterns

Strategy: validation

Validate before calling

// Server-side: assert the gate array is intact before registering:
const gate = createUserProtectedGate(deps, opts);
if (gate.length < 3) throw new Error('userProtected gate must include refreshUser');
app.delete('/user', ...gate, handler);

Prevention

When it happens

Trigger: A route registered only the verifyIdentity handler (or reordered the array) instead of the full `[requireSessionCookie, refreshUser, verifyIdentity]` from `createUserProtectedGate`; a custom wiring that skipped refreshUser.

Common situations: Refactor that picked middleware subsets; copy-paste route registration that omitted refreshUser; someone reordered the chain.

Related errors


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