fix: prevent duplicate _attachment@1 records; simplify GET /attachments#28
Merged
Merged
Conversation
…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
…when filename is provided
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Root bug fix:
POST /attachmentswas callingScopedStack.putAttachment()server-side, which created an_attachment@1metadata record. The adapter-api client'sScopedStack.putAttachment()then created a second one viaPOST /records, producing two metadata records per upload with conflicting data. The route now callsadapter.putAttachment(data)directly (bytes only), matching theStackAdapterinterface contract.GET /attachments — no more DB queries: The download handler no longer queries
_attachment@1for metadata. Callers supply metadata themselves via query params. Without params the response falls back toContent-Type: application/octet-streamwith no filename inContent-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 toapplication/octet-streamin the response regardless of what the caller requests.X-Content-Type-Options: nosniffalone 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@1record 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
?contentTypeContent-Typeon the response (blocked types are sanitized toapplication/octet-stream)?filenameContent-Disposition; also infersContent-Typefrom the file extension when?contentTypeis omittedCallers that have the
_attachment@1record (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 fallbacktests/routes/attachments.test.ts— updated to match new behaviourdocs/api.md— updated POST description