{"record":{"id":"3bc101a2190fdc24","repo":"HeyPuter/puter","slug":"app-not-found-3bc101","errorCode":null,"errorMessage":"App not found","messagePattern":"App not found","errorType":"http","errorClass":"HttpError","httpStatus":404,"severity":"warning","filePath":"extensions/metering.ts","lineNumber":86,"sourceCode":"    req: Request,\n    res: Response,\n): Promise<void> => {\n    const actor = Context.get('actor');\n    if (!actor?.user) throw new HttpError(401, 'Authentication required');\n\n    let appId = String(req.params.appIdOrName ?? '');\n    if (!appId) throw new HttpError(400, 'appId parameter is required');\n\n    // If not a UUID-shaped app UID, look up by name\n    if (!appId.startsWith('app-')) {\n        const appRows = (await clients.db.read(\n            'SELECT `uid` FROM `apps` WHERE `name` = ? LIMIT 1',\n            [appId],\n        )) as Array<{ uid: string }>;\n        if (appRows.length > 0) {\n            appId = appRows[0].uid;\n        } else {\n            throw new HttpError(404, 'App not found');\n        }\n    }\n\n    const appUsage =\n        await services.metering.getActorCurrentMonthAppUsageDetails(\n            actor,\n            appId,\n        );\n    res.json(appUsage);\n};\n\nexport const handleMeteringGlobalUsage = async (\n    _req: Request,\n    res: Response,\n): Promise<void> => {\n    const globalUsage = await services.metering.getGlobalUsage();\n    res.json(globalUsage);\n};","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/extensions/metering.ts#L68-L104","documentation":"When create() is given options.dedupe_name and the requested name already exists, the driver appends a random 4-char suffix and retries up to 4 candidates (i starts at 0; throws when i >= 3 after the increment). If every candidate still collides in existsByName, it gives up with 400 app_name_already_in_use 'Failed to dedupe app name'. This is a backstop for pathological collision, not a normal path.","triggerScenarios":"Many concurrent create() calls with the same base name and dedupe_name:true, so the random suffix space (36^4) collides; or an adversary pre-populating names matching the suffix pattern. Single-threaded calls almost never hit it.","commonSituations":"Bulk-import / seeding scripts that fan out many creates of the same name; a namespace already saturated with '<name>-XXXX' entries; very high concurrency against a small app namespace.","solutions":["Supply a unique name yourself rather than relying on the dedupe retry loop.","Lower concurrency / serialize creates sharing a base name.","On this error, generate a longer/higher-entropy suffix client-side and retry create() without dedupe_name.","Pre-screen candidate names against select() before submitting."],"exampleFix":"// before\nawait puter.apps.create({ name: 'my-app' }, { dedupe_name: true }); // fails under concurrency\n\n// after — supply a unique name directly\nconst unique = `my-app-${crypto.randomUUID().slice(0, 8)}`;\nawait puter.apps.create({ name: unique });","handlingStrategy":"fallback","validationCode":"// Generate a unique name client-side instead of relying on the dedupe loop.\nconst name = `my-app-${crypto.randomUUID().slice(0, 8)}`;\nawait puter.apps.create({ name });","typeGuard":null,"tryCatchPattern":"try { await puter.apps.create({ name: base }, { dedupe_name: true }); }\ncatch (e) {\n  if (e?.code === 'app_name_already_in_use') {\n    await puter.apps.create({ name: `${base}-${crypto.randomUUID().slice(0,8)}` });\n  } else throw e;\n}","preventionTips":["Do not rely on the dedupe retry loop under high concurrency.","Supply your own high-entropy suffix for bulk/seed imports.","Serialize creates that share a base name."],"tags":["apps","dedupe","concurrency","naming"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}