Skip to content

fix(vercel): Blob direct HTTP parity - #304

Merged
scotttrinh merged 7 commits into
mainfrom
blob-direct-http-parity
Oct 14, 2026
Merged

scotttrinh merged 7 commits into
mainfrom
blob-direct-http-parity

Conversation

@scotttrinh

@scotttrinh scotttrinh commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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.

  1. POST /api/blob/delete with a missing or empty urls (including a JSON array, null, or primitive body) returns 400 bad_request Missing urls in request body and deletes nothing.
  2. Malformed JSON or a non-array urls returns 400 Cannot delete more than 1,000 urls at once or body is malformed. More than 1,000 urls returns 400 Cannot delete more than 1,000 urls at once, and any non-string entry returns 400 Some urls are malformed. All of these are checked before anything is deleted.
  3. Copy (PUT ?pathname=&fromUrl=) compares x-if-match with the source blob's ETag, so a matching source ETag copies to a new pathname.
  4. A copy whose x-if-match only matches the destination returns 412 precondition_failed and leaves both blobs untouched. The order is: resolve the source (404 if missing), then check x-if-match against it. Copies always replace an existing destination, matching the real CopyObject call, which has no destination guard.
  5. Folded list() paginates over one sorted list of folder prefixes and root blob pathnames, which is what S3 ListObjectsV2 with 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.
  6. Raw PUT content type is x-content-type, then a known pathname extension, then the request Content-Type, then application/octet-stream. Multipart creation uses the same order.
  7. x-cache-control-max-age below 60 is clamped to 60; absent or unparseable values still default to 2592000.
  8. Exactly one leading slash is stripped from pathname before the random suffix, for uploads, copy destinations, and multipart creation, as getDecodedPathname does in the real service. The emulator's server-side // rejection is removed: the real service never rejects //, so raw HTTP pathname=//a.txt now stores /a.txt and a//b.txt is accepted. The @vercel/blob SDK still throws on // before sending a request, so SDK callers see no change.
  9. POST /api/blob/mpu with x-mpu-action: create returns { key, uploadId } and records a session in a new vercel.blob_uploads collection (store, pathname, upload ID, resolved content type and cache control, createdAt, empty parts) 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.ts so put, copy and multipart creation share them, and ETag normalization moved to etag.ts instead 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 capture x-if-match or x-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 request Content-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 --help doesn't describe Blob behavior, so it isn't changed.

🤖 Generated with Claude Code

Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>
@vercel

vercel Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
emulate Ready Ready Preview, v0 Oct 14, 2026 8:51pm UTC

Co-Authored-By: Scott Trinh <1682194+scotttrinh@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread packages/@emulators/vercel/src/routes/blob/delete.ts
All other falsy values fall through to later error handler

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@scotttrinh
scotttrinh marked this pull request as ready for review October 14, 2026 17:26

@Railly Railly left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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>
Comment thread packages/@emulators/vercel/src/routes/blob/upload.ts Outdated
vercel Bot and others added 2 commits October 14, 2026 20:46
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>
@scotttrinh

Copy link
Copy Markdown
Contributor Author

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 //) to match the service behavior.

@scotttrinh
scotttrinh merged commit 611f424 into main Oct 14, 2026
13 checks passed

This branch was successfully deployed

1 active deployment
Preview — 7def739a Deployed Oct 14, 2026 by vercel[bot]
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.

3 participants