{"record":{"id":"dae3ebae30689c38","repo":"amruthpillai/reactive-resume","slug":"not-found-dae3eb","errorCode":"NOT_FOUND","errorMessage":"NOT_FOUND","messagePattern":"NOT_FOUND","errorType":"error_code","errorClass":"ORPCError","httpStatus":404,"severity":"warning","filePath":"packages/api/src/features/storage/router.ts","lineNumber":103,"sourceCode":"\t\t\tFORBIDDEN: {\n\t\t\t\tmessage: \"You do not have permission to delete this file.\",\n\t\t\t\tstatus: 403,\n\t\t\t},\n\t\t})\n\t\t.handler(async ({ context, input }): Promise<void> => {\n\t\t\tconst requestedKey = normalizeKey(input.filename);\n\t\t\tconst key = requestedKey.startsWith(\"uploads/\")\n\t\t\t\t? requestedKey\n\t\t\t\t: normalizeKey(`uploads/${context.user.id}/pictures/${requestedKey}`);\n\t\t\tconst userPrefix = `uploads/${context.user.id}/`;\n\n\t\t\tif (isUnsafeStorageKey(key) || !key.startsWith(userPrefix)) {\n\t\t\t\tthrow new ORPCError(\"FORBIDDEN\");\n\t\t\t}\n\n\t\t\tconst deleted = await storageService.delete(key);\n\n\t\t\tif (!deleted) throw new ORPCError(\"NOT_FOUND\");\n\t\t}),\n};\n","sourceCodeStart":85,"sourceCodeEnd":106,"githubUrl":"https://github.com/amruthpillai/reactive-resume/blob/3a5b12e2a40374a9571988701fcb75c5a1831c42/packages/api/src/features/storage/router.ts#L85-L106","documentation":"Thrown after storageService.delete(key) returns false, meaning no file or directory existed at the resolved path. The ownership and traversal checks already passed (otherwise FORBIDDEN would have fired first). oRPC raises NOT_FOUND (HTTP 404) — this is a benign 'nothing to delete' outcome.","triggerScenarios":"Deleting a file that was already deleted; a stale URL/path from an older upload; the picture row was cleared but the client still holds the old path; concurrent delete requests racing on the same key.","commonSituations":"User removes a profile picture, clicks delete again; cached UI referencing an uploaded file that a background cleanup reaped; retry of a delete that already succeeded.","solutions":["Treat 404 from deleteFile as success (idempotent delete) in the client and clear the local reference.","Invalidate the relevant query cache so the UI stops offering the missing file for deletion.","Avoid offering a delete action for paths that are no longer present in the server's listing.","If idempotent semantics are desired server-side, consider returning 200 instead of 404 — but do not change behavior without coordinating API consumers."],"exampleFix":"// before: surfacing 404 as an error on delete\ntry { await orpc.storage.deleteFile.mutate({ filename }); }\ncatch (e) { toast.error('Delete failed'); }\n// after: treat 'already gone' as success\ntry { await orpc.storage.deleteFile.mutate({ filename }); }\ncatch (e) {\n  if (!isORPCError(e, 'NOT_FOUND')) throw e;\n}\nqueryClient.invalidateQueries({ queryKey: ['storage'] });","handlingStrategy":"try-catch","validationCode":"const mine = await orpc.storage.list.query({ prefix: `uploads/${userId}/` });\nif (!mine.includes(filename)) { /* already gone — treat as deleted */ }","typeGuard":"function isStorageNotFound(e: unknown): boolean {\n  return e instanceof ORPCError && e.code === 'NOT_FOUND';\n}","tryCatchPattern":"try { await orpc.storage.deleteFile.mutate({ filename }); }\ncatch (e) { if (!isStorageNotFound(e)) throw e; /* idempotent: already deleted */ }","preventionTips":["Treat delete 404 as success; do not alarm the user.","Invalidate the file-list cache after delete so the UI drops the entry.","Disable the delete affordance once the path is no longer listed."],"tags":["storage","not-found","idempotency","orpc"],"backgroundTag":null,"analyzedSha":"3a5b12e2a40374a9571988701fcb75c5a1831c42","analyzedAt":"2026-08-12T22:31:22.666Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}