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

  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.