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.

Solutions

  1. Handle 404 by refreshing the app list and discarding the stale edit form.
  2. Re-fetch the app immediately before editing if the form was open a long time.
  3. Validate the uid format before calling to fail fast on garbage.
  4. 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

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.

Appendix: 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)