Repository navigation
Open
Description
The batch endpoint (POST /v1/objects:batch/{usecase}/{scopes}/) uses multipart/form-data, but all per-operation data lives in custom per-part headers:
- Requests: the server reads
x-sn-batch-operation-kind,x-sn-batch-operation-key, and insert metadata (content-type,x-sn-expiration,x-sn-origin,x-snme-*, …) from each part's headers (objectstore-server/src/extractors/batch.rs).Content-Dispositionis ignored. - Responses: every part is
name=part. Index, status, key, kind, and object metadata are inx-sn-batch-operation-*and metadata headers (objectstore-server/src/endpoints/batch.rs,objectstore-server/src/multipart.rs). Parts stream in completion order, so the index header is the only way to correlate a result with its operation.
The web FormData API (new FormData() + fetch for requests, Response.formData() for responses, also used by Node via undici) only exposes each part's name, filename, content type, and value. JS clients in Node, browsers, or Workers have to hand-roll a byte-level multipart encoder and parser, as getsentry/sentry-api-schema#133 does for the Sentry CLI's snapshot upload.
Tested on Node 22.23 (undici) against the current response shape:
Response.formData()throwsFailed to parse body as FormDatabecause the part name is unquoted (form-data; name=part).- With the name quoted, it parses, but every entry is named
part, all per-part headers are dropped, and parts without a filename come back as strings (even with aContent-Type), which corrupts binary GET payloads.
Raised in review of getsentry/sentry-api-schema#133.
Proposal
- Quick, backward-compatible fix: quote the part name (
form-data; name="part") so standards-based parsers no longer reject the response. This doesn't make the metadata readable, but it removes a spec deviation. - Opt-in FormData-native format that carries metadata in form fields instead of part headers:
- Request: a first part
name="operations"(application/json) with an ordered list, e.g.[{"kind":"insert","key":"a/b","headers":{"content-type":"image/png","x-sn-expiration":"ttl:30d"}}, {"kind":"head","key":"c"}]. Theheadersmap keeps existing names so the server can reuseMetadata::from_insert_headers. Insert bodies follow as parts named by operation index (name="0", with a filename and Blob), so a client just doesform.append("0", blob, "0"). Because the manifest comes first, the server can still stream body parts. - Response: for each completed operation, a JSON part
name="<index>"such as{"status":201,"kind":"insert","key":"a/b","headers":{…}}(error body included on failure). For a GET with a payload, a following partname="<index>.body"withfilenameandContent-Typeset, so parsers return aFileand binary stays intact. Completion-order streaming is preserved. - Negotiation: the server detects the request format from a leading
operationspart. The client opts into the new response format explicitly (e.g.?format=formor anAcceptprofile parameter). Without it, the server keeps today's header format, so the Rust client and the in-progress Python batch client (feat(py-client): Implement "many" api for batch requests #546) are unaffected.
- Request: a first part
Alternatives
- Status quo: keep the header protocol and maintain a hand-rolled parser in each JS client (~200 lines that must track the server).
- Metadata in
name/filenameonly (e.g.name="insert:3",filename=<key>): can't carry expiration or custom metadata, and filename escaping differs between implementations. - JSON body with base64 payloads: ~33% size overhead and no streaming.
- Emit both formats at once: breaks existing clients, which expect one part per operation with an index header.
Open questions
- Should the new format replace the per-part headers or sit alongside them?
Response.formData()buffers the whole response; should responses in the new format have a lower size cap?
via @BYK.
--
Metadata
Metadata
Assignees
Labels
No labels
Activity