{"record":{"id":"2749f662153baf17","repo":"HeyPuter/puter","slug":"bad-request-2749f6","errorCode":"bad_request","errorMessage":"No ETag for ${filePath} part ${partNumber}","messagePattern":"No ETag for (.+?) part (.+?)","errorType":"validation","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"src/backend/clients/s3/S3Client.ts","lineNumber":366,"sourceCode":"                if (bytesRead <= 0) break;\n\n                const body =\n                    bytesRead === partBuffer.length\n                        ? partBuffer\n                        : partBuffer.subarray(0, bytesRead);\n                const { ETag } = await client.send(\n                    new UploadPartCommand({\n                        Bucket: bucket,\n                        ContentLength: bytesRead,\n                        Key: key,\n                        PartNumber: partNumber,\n                        UploadId,\n                        Body: body,\n                    }),\n                );\n\n                if (!ETag)\n                    throw new HttpError(\n                        400,\n                        `No ETag for ${filePath} part ${partNumber}`,\n                        { legacyCode: 'bad_request' },\n                    );\n                uploadedParts.push({ ETag, PartNumber: partNumber });\n\n                offset += bytesRead;\n                partNumber++;\n            }\n\n            await client.send(\n                new CompleteMultipartUploadCommand({\n                    Bucket: bucket,\n                    Key: key,\n                    UploadId,\n                    MultipartUpload: { Parts: uploadedParts },\n                }),\n            );","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/clients/s3/S3Client.ts#L348-L384","documentation":"AppDriver.read resolves { uid, id } against the store; a null result means no app matched either key. Thrown as 404 not_found after actor resolution but before access checks, so it does not leak whether an app exists that the caller can't read. Both uid and id are accepted lookup keys.","triggerScenarios":"Calling puter.apps.get(uid) or get(id) with a value that matches no app row — typo, deleted app, wrong environment (dev uid sent to prod), or a uid copied from a different object type.","commonSituations":"Stale uid cached client-side after the app was deleted; cross-environment uid (test → prod); user pasted a file uid instead of an app uid; id confusion (numeric id vs uid string).","solutions":["Verify the uid/id source — confirm it came from a prior create()/list() in the same environment.","Treat 404 as 'not found or deleted' and refresh the client's app list.","Validate uid format (UUID-ish) before calling to fail fast on garbage.","If the app was deleted, offer to re-create rather than silently looping."],"exampleFix":"// before\nconst app = await puter.apps.get(staleUid);\n\n// after — handle not-found explicitly\nlet app;\ntry { app = await puter.apps.get(storedUid); }\ncatch (e) {\n  if (e?.code === 'not_found') { refreshAppList(); return; }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// Fail fast on malformed uids before the round trip.\nfunction looksLikeUid(v) { return typeof v === 'string' && /^[A-Za-z0-9_-]{8,}$/.test(v); }\nif (!looksLikeUid(storedUid)) { refreshAppList(); return; }","typeGuard":null,"tryCatchPattern":"try { return await puter.apps.get(storedUid); }\ncatch (e) {\n  if (e?.code === 'not_found') { refreshAppList(); notify('App not found.'); return null; }\n  throw e;\n}","preventionTips":["Source uids only from prior create()/list() results in the same environment.","Refresh cached uids when a 404 occurs rather than retrying the same value.","Do not mix dev and prod uids."],"tags":["apps","not-found","lookup"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}