HeyPuter/puter · error · HttpError
forbidden
forbidden
Error message
Access tokens cannot report app opens
What it means
AppDriver.update resolves { uid, id } against the store; a null result throws 404 not_found. This runs after the object-shape check but before #checkWriteAccess, so — like read — it does not leak the existence of apps the caller cannot edit. Same lookup path as read(), just on the update branch.
Source
Thrown at src/backend/controllers/apps/AppController.js:226
const actor = req.actor;
const bodyAppUid = req.body?.app_uid;
const actorAppUid = actor?.app?.uid;
const app_uid =
typeof bodyAppUid === 'string' && bodyAppUid.length > 0
? bodyAppUid
: actorAppUid;
if (!app_uid || typeof app_uid !== 'string') {
throw new HttpError(400, 'Missing or invalid `app_uid`', {
legacyCode: 'bad_request',
});
}
// Access tokens (and any other non-user/non-app identity,
// e.g. asset tokens) are not allowed to report opens —
// they're shared / scoped credentials and shouldn't drive
// analytics counters.
if (isAccessTokenActor(actor)) {
throw new HttpError(
403,
'Access tokens cannot report app opens',
{ legacyCode: 'forbidden' },
);
}
if (isAppActor(actor) && app_uid !== actorAppUid) {
throw new HttpError(
403,
'App actors can only report opens for their own app',
{ legacyCode: 'forbidden' },
);
}
const app = await this.appStore.getByUid(app_uid);
if (!app)
throw new HttpError(404, 'App not found', {
legacyCode: 'not_found',View on GitHub (pinned to 908ec23eda)
Solutions
- Handle 404 by refreshing the app list and discarding the stale edit form.
- Re-fetch the app immediately before editing if the form was open a long time.
- Validate the uid format before calling to fail fast on garbage.
- Confirm the uid came from the same environment (dev/prod) as the API target.
Example fix
// before
await puter.apps.update(staleUid, { name: 'new' });
// after — handle not-found gracefully
try { await puter.apps.update(storedUid, { name: 'new' }); }
catch (e) {
if (e?.code === 'not_found') { refreshAppList(); notify('This app no longer exists.'); return; }
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
function looksLikeUid(v) { return typeof v === 'string' && /^[A-Za-z0-9_-]{8,}$/.test(v); }
if (!looksLikeUid(storedUid)) { refreshAppList(); return; } Try / catch
try { await puter.apps.update(storedUid, patch); }
catch (e) {
if (e?.code === 'not_found') { refreshAppList(); notify('This app no longer exists.'); return; }
throw e;
} Prevention
- Re-fetch the app before opening a long-lived edit form to catch deletions.
- Handle 404 by discarding the stale form, not by retrying the same uid.
- Keep uids environment-scoped to avoid dev/prod drift.
When it happens
Trigger: Calling puter.apps.update(uid, patch) for an app that does not exist: deleted between the get() and the update(), typo in the uid, or a uid from a different environment.
Common situations: Optimistic UI editing a row another user/process deleted; cross-environment uid drift; id/uid mix-up; long-lived edit form whose target was removed.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/451b31c7fc21a99d.
Report an issue: GitHub.