{"record":{"id":"c7c978c59bd2a041","repo":"can1357/oh-my-pi","slug":"cannot-delete-an-invalid-anthropic-file-handle","errorCode":null,"errorMessage":"Cannot delete an invalid Anthropic file handle","messagePattern":"Cannot delete an invalid Anthropic file handle","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/provider-files-anthropic.ts","lineNumber":135,"sourceCode":"\t\t\t\t\tbody: form,\n\t\t\t\t\tsignal: request.signal,\n\t\t\t\t}),\n\t\t\t\t\"upload\",\n\t\t\t);\n\t\t\tconst metadata = parseMetadata(await response.json());\n\t\t\tconst deleteUrl = `${ANTHROPIC_FILES_URL}/${encodeURIComponent(metadata.id)}`;\n\t\t\treturn {\n\t\t\t\tprovider: \"anthropic\",\n\t\t\t\tid: metadata.id,\n\t\t\t\tmimeType: metadata.mime_type,\n\t\t\t\tbytes: metadata.size_bytes,\n\t\t\t\texpiresAt: parseExpiresAt(metadata.expires_at),\n\t\t\t\tdelete: { method: \"DELETE\", url: deleteUrl, headers },\n\t\t\t};\n\t\t},\n\t\tasync delete(handle: ProviderFileHandle): Promise<void> {\n\t\t\tif (handle.provider !== \"anthropic\" || !handle.id)\n\t\t\t\tthrow new Error(\"Cannot delete an invalid Anthropic file handle\");\n\t\t\tawait expectAnthropicOk(\n\t\t\t\tawait fetchImpl(`${ANTHROPIC_FILES_URL}/${encodeURIComponent(handle.id)}`, {\n\t\t\t\t\tmethod: \"DELETE\",\n\t\t\t\t\theaders,\n\t\t\t\t}),\n\t\t\t\t\"delete\",\n\t\t\t);\n\t\t},\n\t};\n}\n","sourceCodeStart":117,"sourceCodeEnd":146,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/provider-files-anthropic.ts#L117-L146","documentation":"`delete` performs a DELETE against the Anthropic Files API using `handle.id`, so it first verifies the handle actually belongs to this provider and carries a file id. If `handle.provider !== \"anthropic\"` or `handle.id` is falsy, the handle was not produced by this client (or was corrupted/hand-built), and calling DELETE with a missing id would hit the collection URL instead of a file resource — so the client throws a fail-fast validation error instead.","triggerScenarios":"`delete(handle)` is called with a handle whose `provider` field is not \"anthropic\" (e.g. a gemini or generic handle), or a handle object missing `id` — typically a handle built by hand, deserialized from persisted state with fields lost, or passed to the wrong provider's client.","commonSituations":"Mixing handles across provider clients (Gemini handle passed to the Anthropic client or vice versa); restoring handles from a database/JSON session where `id` was never persisted or renamed; constructing `{ provider: \"anthropic\" }` manually without the id; a refactor changed the handle shape and old stored handles no longer match.","solutions":["Inspect the handle just before deletion: confirm `handle.provider === \"anthropic\"` and `typeof handle.id === \"string\" && handle.id.length > 0`.","Only pass handles that came from this client's `upload()` return value — do not build handles manually or pass handles from a different provider client.","If handles are persisted, store the full object including `id` and `provider`, and validate on load before attempting cleanup.","Add a migration/repair step for stored handles created before a shape change, or discard unrepairable handles and skip their deletion (the remote file will expire per its TTL)."],"exampleFix":"// before — deleting whatever handle is in the session\nawait anthropicClient.delete(session.fileHandle);\n\n// after — validate the handle first\nconst handle = session.fileHandle;\nif (handle?.provider === \"anthropic\" && typeof handle.id === \"string\" && handle.id) {\n  await anthropicClient.delete(handle);\n} else {\n  logger.warn(\"Skipping cleanup: invalid or non-Anthropic file handle\", { handle });\n}","handlingStrategy":"type-guard","validationCode":"// validate handles before cleanup, especially if restored from storage\nfunction isDeletableAnthropicHandle(h: unknown): h is { provider: \"anthropic\"; id: string } {\n  if (typeof h !== \"object\" || h === null) return false;\n  const r = h as Record<string, unknown>;\n  return r.provider === \"anthropic\" && typeof r.id === \"string\" && r.id.length > 0;\n}","typeGuard":"const isAnthropicFileHandle = (h: ProviderFileHandle): h is ProviderFileHandle & { provider: \"anthropic\"; id: string } => h.provider === \"anthropic\" && typeof h.id === \"string\" && h.id.length > 0;","tryCatchPattern":"try {\n  await anthropicFiles.delete(handle);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"invalid Anthropic file handle\")) {\n    logger.warn(\"Skipping cleanup of malformed file handle\", { provider: handle?.provider });\n    // remote file will expire via TTL; do not crash the cleanup loop\n  } else throw err;\n}","preventionTips":["Always pass handles obtained from the same provider client's upload(); never mix providers or build handles by hand.","Persist handles in full (provider + id) and validate on restore before scheduling deletion.","Make cleanup loops skip-and-log on invalid handles rather than letting one bad handle abort the rest.","After any handle-shape refactor, add a migration check for previously stored handles."],"tags":["anthropic","files-api","validation","cleanup","handle-mismatch"],"backgroundTag":"invalid-file-handle","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}