Skip to content

fix: prevent duplicate _attachment@1 records; simplify GET /attachments#28

Merged
cuibonobo merged 7 commits into
mainfrom
claude/adapter-api-duplicate-attachments-myku08
Jun 23, 2026
Merged

fix: prevent duplicate _attachment@1 records; simplify GET /attachments#28
cuibonobo merged 7 commits into
mainfrom
claude/adapter-api-duplicate-attachments-myku08

Conversation

@cuibonobo

@cuibonobo cuibonobo commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

  • Root bug fix: POST /attachments was calling ScopedStack.putAttachment() server-side, which created an _attachment@1 metadata record. The adapter-api client's ScopedStack.putAttachment() then created a second one via POST /records, producing two metadata records per upload with conflicting data. The route now calls adapter.putAttachment(data) directly (bytes only), matching the StackAdapter interface contract.

  • GET /attachments — no more DB queries: The download handler no longer queries _attachment@1 for metadata. Callers supply metadata themselves via query params. Without params the response falls back to Content-Type: application/octet-stream with no filename in Content-Disposition.

  • MIME type sanitization on download: Dangerous types (text/html, image/svg+xml, text/javascript, application/javascript, application/xhtml+xml, text/xml, application/xml, application/x-javascript) are forced to application/octet-stream in the response regardless of what the caller requests. X-Content-Type-Options: nosniff alone is insufficient because it only prevents browser type guessing — it does not block execution of an explicitly-declared dangerous type.

  • DELETE fallback: When no _attachment@1 record exists for a file, the handler now checks byte existence in the adapter directly, so orphaned files (bytes with no metadata record) remain deletable.

GET /attachments/:fileId query params

Parameter Effect
?contentType Sets Content-Type on the response (blocked types are sanitized to application/octet-stream)
?filename Sets the filename in Content-Disposition; also infers Content-Type from the file extension when ?contentType is omitted

Callers that have the _attachment@1 record (e.g. after querying for it) can pass ?contentType=<mimeType>&filename=<name> to get a fully-described download without the server performing any additional DB work.

Files changed

  • src/routes/attachments.ts — POST bytes-only; GET no DB query + MIME sanitization + query params; DELETE orphan fallback
  • tests/routes/attachments.test.ts — updated to match new behaviour
  • docs/api.md — updated POST description

cuibonobo and others added 7 commits June 22, 2026 19:28
…hment@1 records

When the adapter-api calls POST /attachments, the server was calling
ScopedStack.putAttachment() which stores the bytes AND creates an
_attachment@1 metadata record. The client-side ScopedStack.putAttachment()
then also created a second _attachment@1 record via POST /records, because
it treats adapter.putAttachment() as bytes-only per the StackAdapter
interface contract.

This produced two _attachment@1 records per upload:
- Record #1: created by the server route, with mimeType hardcoded to
  application/octet-stream (APIAdapter always sends that) and no filename
- Record #2: created by the client ScopedStack, with correct mimeType
  and filename

Fix: the POST /attachments route now calls adapter.putAttachment(data)
directly (bytes only), matching the StackAdapter interface intent. The
metadata record is the caller's responsibility, which ScopedStack handles
automatically via its subsequent POST /records call.

Downstream changes:
- GET /attachments/:fileId: falls back to application/octet-stream when
  no _attachment@1 record exists (instead of 404), since the file may
  exist without a metadata record immediately after upload
- DELETE /attachments/:fileId: falls back to checking bytes existence
  when no _attachment@1 record is found, so orphaned bytes are deletable
…rs supply metadata via query params

The GET handler no longer queries the database for metadata. Callers pass
?contentType and/or ?filename as query params; without them the response
falls back to application/octet-stream with no Content-Disposition filename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HteEBim64NucEifnFH1yqQ
@cuibonobo cuibonobo changed the title Separate attachment upload from metadata record creation fix: prevent duplicate _attachment@1 records; simplify GET /attachments Jun 23, 2026
@cuibonobo cuibonobo merged commit b1a6a0d into main Jun 23, 2026
4 checks passed
@cuibonobo cuibonobo deleted the claude/adapter-api-duplicate-attachments-myku08 branch June 23, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants