HeyPuter/puter · error · HttpError

access_denied

access_denied

Error message

Access denied

What it means

Thrown by FSController.#assertAccess when the ACL check denies access and `acl.getSafeAclError` reports a 404 status. The 404 path means the actor is treated as unable to even *see* the resource — the system declines to confirm the entry exists to that actor. This is the standard 'deny hides existence' behavior layered on top of ACL evaluation.

Solutions

  1. Confirm the path is shared with or owned by the actor before issuing the call.
  2. Re-issue with an explicit uid of an entry the actor can see, or request access from the owner.
  3. Treat the 404 as authoritative in the client — do not assume a later retry will succeed without a permission change.
  4. Audit ACLService rules / shares for the path if access was expected.
Defensive patterns

Strategy: try-catch

Try / catch

try { await op(); }
catch (e) {
  if (e?.status === 404 && /denied|access/i.test(e.message)) {
    // entry not visible to actor; do not disclose existence to end user
  } else throw e;
}

Prevention

When it happens

Trigger: Calling an FS read/write operation on a path the actor has no `see`/`list` permission on, where the ACL service returns a not-found-style safe error. Common with private folders under another user's home, or paths inside an app's AppData owned by a different app uid.

Common situations: App attempting to read outside its AppData scope; user navigating to a path they can't see; deleted-from-share target; an ACL cache that hasn't propagated a newly-granted permission.

Understand the failure class

Related errors


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

Appendix: source

Thrown at src/backend/controllers/fs/FSController.ts:1747

            descriptor,
            mode,
        )) as {
            status?: unknown;
            message?: unknown;
            fields?: { code?: unknown };
        };
        const status = Number(safe?.status);
        const message =
            typeof safe?.message === 'string' && safe.message.length > 0
                ? safe.message
                : 'Access denied';
        const code =
            typeof safe?.fields?.code === 'string'
                ? safe.fields.code
                : undefined;
        const legacyCode = code === 'forbidden' ? 'access_denied' : code;
        if (status === 404) {
            throw new HttpError(404, message, {
                ...(legacyCode ? { legacyCode } : {}),
            });
        }
        throw new HttpError(403, message, {
            legacyCode: legacyCode ?? 'access_denied',
        });
    }

    #toNumberOrUndefined(value: unknown): number | undefined {
        if (typeof value === 'number' && Number.isFinite(value)) return value;
        if (typeof value === 'string' && value.trim().length > 0) {
            const parsed = Number(value);
            if (Number.isFinite(parsed)) return parsed;
        }
        return undefined;
    }

    // Accepts loose inputs from route bodies. `source`/`destination` fields may

View on GitHub (pinned to 908ec23eda)