Skip to content

fix(blobs): throw when a conditional write fails - #756

Open
mrpmohiburrahman wants to merge 1 commit into
netlify:mainfrom
mrpmohiburrahman:fix/conditional-write-swallows-failures
Open

fix(blobs): throw when a conditional write fails#756
mrpmohiburrahman wants to merge 1 commit into
netlify:mainfrom
mrpmohiburrahman:fix/conditional-write-swallows-failures

Conversation

@mrpmohiburrahman

Copy link
Copy Markdown

A conditional write reports success for every failure that isn't a 412.

set and setJSON both decide the result with the same two-armed ternary:

if (conditions) {
  return res.status === STATUS_PRE_CONDITION_FAILED ? { modified: false } : { etag, modified: true }
}

412 means the precondition failed. Everything else counts as a successful write.
And since that returns before the if (res.status === STATUS_OK) check below it,
the throw new BlobsInternalError(...) that ends both methods can never be
reached on this path. fetchAndRetry returns the last failing response instead
of throwing, so a store that is down for all six attempts arrives here as an
ordinary 503 and gets reported as { etag: '', modified: true }.

Drop onlyIfMatch / onlyIfNew and the same call throws correctly. So only the
conditional form swallows the failure, and that's the form whose return value is
the caller's only signal that anything landed.

Both blocks now check 412 first, treat any 2xx as success, and let everything
else fall through to the BlobsInternalError that was already there.

I used res.ok rather than res.status === STATUS_OK on purpose. Conditional
writes succeed with 201, and four existing tests say so (onlyIfNew success, API
and edge, both methods). Narrowing to 200 turns those four red, which trades a
silent lost write for a spurious failure. The unconditional path is untouched, so
the two paths now accept different success sets. That asymmetry is intentional
here, though it might deserve its own look.

Fixes #741

Tests

Six added, spread across the four existing Conditional writes blocks:

  • a non-retried failing status (400) for set and setJSON on both the API and
    edge paths, one of them via onlyIfNew so if-none-match is covered too
  • the reporter's exact scenario, a 503 on every attempt until retries run out,
    for set and setJSON

Each one asserts BlobsInternalError and the message, so the status that reached
the throw is pinned rather than assumed.

packages/blobs goes from 140 to 146 passing with nothing regressed. The four 201
tests above guard the res.ok choice and stay green.

Notes

  • This changes behavior for anyone doing a fire-and-forget conditional write with
    no try/catch. They get a rejection now where they used to get a false
    success. That is what the issue asks for and what unconditional writes already
    do, but I titled it fix, so relabel it if you would rather ship a feat!.
  • 412 is still the only status mapped to { modified: false }. On the
    API-credentials path the conditional PUT goes to a signed provider URL, so if
    that provider answers a failed if-match with a 404 instead of a 412, this
    turns it into a throw. It isn't a regression, since that case returned a
    phantom success before, but someone who knows the provider's status matrix
    should probably check.
  • The corrected block is identical in set and setJSON. I left it duplicated
    to keep the diff shaped like fix(blobs): send conditional write headers in setJSON #731. Happy to pull it into a private helper next
    to getConditions if you would rather have that.

Conditional writes reported `{ etag: '', modified: true }` for every response
that was not a 412, including a 503 after retries were exhausted. They now throw
`BlobsInternalError`, the way unconditional writes already do. This changes
behavior: callers who never wrapped a conditional write in try/catch will get a
rejection where they used to get a false success.
@mrpmohiburrahman
mrpmohiburrahman requested a review from a team as a code owner August 20, 2026 02:15
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 11831bf0-824c-4a09-9934-75826f5da95a

📥 Commits

Reviewing files that changed from the base of the PR and between e5050be and 993edb6.

📒 Files selected for processing (2)
  • packages/blobs/src/main.test.ts
  • packages/blobs/src/store.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • netlify/blueprints (manual)

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved conditional blob writes to correctly recognize successful responses.
    • Conditional writes now report unmodified results for precondition failures.
    • Other failed write requests now return clear errors instead of being treated as successful.
    • Added retry handling for temporary service-unavailable responses.

Walkthrough

Conditional set and setJSON operations now treat successful 2xx responses as modified writes, return modified: false for HTTP 412, and throw for other failures. Tests cover HTTP 400 errors and exhausted HTTP 503 retries for API and edge credentials.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 993ed

The change makes conditional writes report failed responses as errors while preserving successful 2xx writes and adds focused coverage; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: paulo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the fix: conditional writes now throw when they fail.
Description check ✅ Passed The description accurately explains the conditional-write bug, status handling, tests, and intended behavior.
Linked Issues check ✅ Passed The changes satisfy [#741] by preserving 412 handling, accepting 2xx success, and throwing BlobsInternalError for other failures.
Out of Scope Changes check ✅ Passed The code and tests remain within the linked issue scope for conditional blob write error handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@netlify/ai

npm i https://pkg.pr.new/@netlify/ai@756

@netlify/aws-lambda-compat

npm i https://pkg.pr.new/@netlify/aws-lambda-compat@756

@netlify/blobs

npm i https://pkg.pr.new/@netlify/blobs@756

@netlify/cache

npm i https://pkg.pr.new/@netlify/cache@756

@netlify/dev

npm i https://pkg.pr.new/@netlify/dev@756

@netlify/dev-utils

npm i https://pkg.pr.new/@netlify/dev-utils@756

@netlify/headers

npm i https://pkg.pr.new/@netlify/headers@756

@netlify/images

npm i https://pkg.pr.new/@netlify/images@756

@netlify/otel

npm i https://pkg.pr.new/@netlify/otel@756

@netlify/redirects

npm i https://pkg.pr.new/@netlify/redirects@756

@netlify/runtime

npm i https://pkg.pr.new/@netlify/runtime@756

@netlify/runtime-utils

npm i https://pkg.pr.new/@netlify/runtime-utils@756

@netlify/static

npm i https://pkg.pr.new/@netlify/static@756

@netlify/types

npm i https://pkg.pr.new/@netlify/types@756

@netlify/database-dev

npm i https://pkg.pr.new/@netlify/database-dev@756

@netlify/database

npm i https://pkg.pr.new/@netlify/database@756

@netlify/database-proxy

npm i https://pkg.pr.new/@netlify/database-proxy@756

@netlify/edge-functions-dev

npm i https://pkg.pr.new/@netlify/edge-functions-dev@756

@netlify/edge-functions

npm i https://pkg.pr.new/@netlify/edge-functions@756

@netlify/functions-dev

npm i https://pkg.pr.new/@netlify/functions-dev@756

@netlify/functions

npm i https://pkg.pr.new/@netlify/functions@756

@netlify/identity

npm i https://pkg.pr.new/@netlify/identity@756

commit: 993edb6

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.

@netlify/blobs: conditional writes report { modified: true } for every non-412 failure (silent lost write)

1 participant