Repository navigation
fix(vercel): Blob direct HTTP parity - #304
Conversation
Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Falsy non-array urls values return the wrong error, and multipart session persistence is not verified.
1 open finding
What changed in this PR
Aligns the Vercel Blob emulator’s HTTP behavior with the real service.
Changes:
- Adds shared upload validation, metadata resolution, and ETag handling.
- Corrects deletion, copy, folded pagination, and cache behavior.
- Adds multipart-session creation with documentation and tests.
| File | Description |
|---|---|
README.md |
Documents updated Blob behavior. |
apps/web/content/docs/vercel.mdx |
Updates web documentation. |
skills/vercel/SKILL.md |
Updates agent usage guidance. |
packages/@emulators/vercel/src/entities.ts |
Defines multipart entities. |
packages/@emulators/vercel/src/store.ts |
Adds multipart-session storage. |
packages/@emulators/vercel/src/routes/blob/constants.ts |
Adds cache and deletion limits. |
packages/@emulators/vercel/src/routes/blob/delete.ts |
Validates deletion requests. |
packages/@emulators/vercel/src/routes/blob/etag.ts |
Centralizes ETag comparison. |
packages/@emulators/vercel/src/routes/blob/head-list.ts |
Corrects folded pagination. |
packages/@emulators/vercel/src/routes/blob/multipart.ts |
Implements multipart creation. |
packages/@emulators/vercel/src/routes/blob/paths.ts |
Separates known MIME inference. |
packages/@emulators/vercel/src/routes/blob/put.ts |
Corrects upload and copy behavior. |
packages/@emulators/vercel/src/routes/blob/upload.ts |
Adds shared upload helpers. |
packages/@emulators/vercel/src/__tests__/blob.test.ts |
Enables parity tests. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
All other falsy values fall through to later error handler Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
@scotttrinh one small thing: the leading slash is stripped before the // check, so pathname=//a.txt now uploads as /a.txt instead of 400 like main. checking // on the raw value first fixes it. ci failure looks like the flaky github test.
Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>
The real service treats any falsy urls value as missing. Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>
Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>
Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>
|
Alright, so I churned a bunch on review feedback, but it turns out what was here before was actually the real behavior of the service, so I reverted all of the changes (early falsy return, and rejection of leading |

The Blob emulator drifted from the real service in a handful of places that only show up when you talk to it over HTTP directly or hit its edges through the SDK. Checked against the actual service for parity.
POST /api/blob/deletewith a missing or emptyurls(including a JSON array,null, or primitive body) returns 400bad_requestMissing urls in request bodyand deletes nothing.urlsreturns 400Cannot delete more than 1,000 urls at once or body is malformed. More than 1,000 urls returns 400Cannot delete more than 1,000 urls at once, and any non-string entry returns 400Some urls are malformed. All of these are checked before anything is deleted.PUT ?pathname=&fromUrl=) comparesx-if-matchwith the source blob's ETag, so a matching source ETag copies to a new pathname.x-if-matchonly matches the destination returns 412precondition_failedand leaves both blobs untouched. The order is: resolve the source (404 if missing), then checkx-if-matchagainst it. Copies always replace an existing destination, matching the realCopyObjectcall, which has no destination guard.list()paginates over one sorted list of folder prefixes and root blob pathnames, which is what S3ListObjectsV2with a delimiter does. A prefix holding only folders now pages one folder at a time instead of returning every folder on every page. Expanded mode is unchanged.x-content-type, then a known pathname extension, then the requestContent-Type, thenapplication/octet-stream. Multipart creation uses the same order.x-cache-control-max-agebelow 60 is clamped to 60; absent or unparseable values still default to 2592000.pathnamebefore the random suffix, for uploads, copy destinations, and multipart creation, asgetDecodedPathnamedoes in the real service. The emulator's server-side//rejection is removed: the real service never rejects//, so raw HTTPpathname=//a.txtnow stores/a.txtanda//b.txtis accepted. The@vercel/blobSDK still throws on//before sending a request, so SDK callers see no change.POST /api/blob/mpuwithx-mpu-action: createreturns{ key, uploadId }and records a session in a newvercel.blob_uploadscollection (store, pathname, upload ID, resolved content type and cache control,createdAt, emptyparts) without publishing anything. Part upload and completion still return the existing 400.Upload auth, pathname normalization, content type, the access check and the random suffix now live in
upload.tsso put, copy and multipart creation share them, and ETag normalization moved toetag.tsinstead of being duplicated a third time. The multipart change is kept small on purpose, since part upload and completion are coming separately. The session deliberately does not capturex-if-matchorx-allow-overwrite: the real service reads those from the completion request, so that work should too. No multipart PR was open when this was written, so test 9 is included here.A few deliberate gaps remain. The real service gates the 60 second cache floor, the random suffix default, and several response shapes on
x-api-version(10 and up for the clamp); the SDK always sends 12, so nothing here reads the version and pre-v12 behavior is otherwise untouched. The list cursor is the last key rather than an opaque S3 token, which the SDK can't tell apart. Copies still ignore the requestContent-Type(the real service doesn't, but the SDK never sends one), and a missing copy source returns 404 where the real service likely surfaces S3's 400.npx emulate --helpdoesn't describe Blob behavior, so it isn't changed.🤖 Generated with Claude Code