HeyPuter/puter · error · HttpError
unauthorized
unauthorized
Error message
Unauthorized
What it means
Thrown by POST /auth/request-app-root-dir after the app ownership check passes, when the app-under-user actor has no user.username. The root path is built as `/<username>/AppData/<appUid>`, so a missing username means the path cannot be constructed and the session is incomplete — rejected as unauthorized.
Source
Thrown at src/backend/controllers/fs/LegacyFSController.ts:1819
const appUid = getString(body, 'app_uid');
if (!appUid)
throw new HttpError(400, '`app_uid` is required', {
legacyCode: 'bad_request',
});
const actorApp = (actor as { app?: { uid?: string } }).app;
if (!actorApp?.uid || actorApp.uid !== appUid) {
throw new HttpError(
403,
'Only the app itself may request its root dir',
{ legacyCode: 'forbidden' },
);
}
const userId = this.#getActorUserId(req);
const username = (actor as { user?: { username?: string } }).user
?.username;
if (!username)
throw new HttpError(401, 'Unauthorized', {
legacyCode: 'unauthorized',
});
const rootPath = `/${username}/AppData/${appUid}`;
// Auto-create the AppData/<uid> tree on first call.
const entry = await this.services.fs.mkdir(userId, {
path: rootPath,
createMissingParents: true,
});
res.json(await toLegacyEntry(this.clients.event, entry));
};
/**
* POST /auth/check-app-acl — check whether an app has a given mode of
* access to a subject FS entry.
*/
checkAppAcl = async (req: Request, res: Response): Promise<void> => {
this.#requireActor(req);View on GitHub (pinned to 908ec23eda)
Solutions
- Use an app-under-user token minted via getUserAppToken so the user (with username) is bound.
- Verify the user record has a username before issuing the token.
- Re-authenticate the user to refresh the session's user binding.
Example fix
// before — app token with no user binding
await fetch('/auth/request-app-root-dir', { method:'POST', headers:{Authorization:'Bearer '+appOnlyToken}, body: JSON.stringify({ app_uid }) })
// after — app-under-user token
const { token } = await puter.auth.getUserAppToken(app_uid)
await fetch('/auth/request-app-root-dir', { method:'POST', headers:{Authorization:'Bearer '+token}, body: JSON.stringify({ app_uid }) }) Defensive patterns
Strategy: validation
Validate before calling
// ensure the app-under-user actor has a username before calling
function assertAppHasUsername(actor) {
const u = actor?.user?.username;
if (typeof u !== 'string' || u.length === 0) {
throw new Error('App session lacks user.username; mint an app-under-user token');
}
} Type guard
/** @param {unknown} a @returns {a is { user: { username: string }, app: { uid: string } }} */
function isAppUnderUserWithUsername(a) {
return !!a && typeof a === 'object' &&
typeof a.user?.username === 'string' && typeof a.app?.uid === 'string';
} Prevention
- Mint the token via getUserAppToken so the user binding is complete.
- Re-authenticate the user if the username binding is missing.
- In tests, build the actor with a populated user.username.
When it happens
Trigger: An app token minted without a bound user context (app present, user missing); a malformed actor in tests; a token that lost its user binding.
Common situations: App token generated by a non-user flow; partial actor object; user record missing username after a partial migration.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/84f6fb4fbac3f239.
Report an issue: GitHub.