Skip to content

fix(net): make fetch behave like native fetch#1652

Merged
jaredwray merged 4 commits into
mainfrom
claude/blissful-dijkstra-WESGR
Jun 4, 2026
Merged

fix(net): make fetch behave like native fetch#1652
jaredwray merged 4 commits into
mainfrom
claude/blissful-dijkstra-WESGR

Conversation

@jaredwray
Copy link
Copy Markdown
Owner

Problem

@cacheable/net's fetch diverged from native fetch in ways that produced "a ton of bugs" for consumers. The biggest one: it threw Error("Fetch failed with status …") on any non-2xx response.

Native fetch never rejects on HTTP status — it resolves with a Response whose .ok is false for 4xx/5xx and only rejects on genuine network errors. Code written for native-fetch semantics checks response.ok, but with @cacheable/net that check became unreachable dead code because the call already threw, surfacing as cryptic errors (e.g. routing a non-cacheable subscription POST through the client and getting an opaque throw instead of a 4xx Response).

All four throw sites were wrapped in /* c8 ignore */ / /* v8 ignore */ and had zero tests — the throwing behavior was never validated.

Changes

Aligned @cacheable/net with native fetch (packages/net/src/fetch.ts, index.ts):

Divergence Fix
Threw on non-2xx (4 sites) Resolve with the Response regardless of status; reject only on network errors
Would cache error responses Cache only storable responses — simple mode caches 2xx only; HTTP-cache mode keeps deferring to RFC 7234 storable(). Errors are returned, never cached
fetch(url, options) required optionsfetch(url) crashed Made options optional, matching fetch(input, init?) and the documented signature
Reconstructed responses dropped .url (→ ""), .redirected, .type Added a shared makeResponse helper that reattaches url (and redirected/type from live responses), so the final URL survives caching and the get/post/… helpers

Tests

Added deterministic, self-contained local-server tests (no external dependency) in both fetch.test.ts and index.test.ts covering:

  • Non-2xx resolves without throwing across every path: no cache, simple cache, HTTP cache, POST/HEAD, the get/post helpers, and CacheableNet methods.
  • Error responses are not cached in simple mode; successful responses still are.
  • fetch(url) works with no options at all.
  • response.url (and redirected) are preserved through reconstruction/caching and the helpers.

All new tests pass; pnpm build, tsc --noEmit, and strict Biome lint are green. The pre-existing mock-backed suite (Docker jaredwray/mockhttp on :3737) is unchanged and runs in CI; the del()-without-options validation throw is intentionally retained.

Behavior change note

Removing the throw is a deliberate behavior change: callers that wrapped fetch in try/catch to catch HTTP errors will now receive a resolved Response. They should switch to checking response.ok — the README's new "Error Handling" section documents this.

https://claude.ai/code/session_01QeG7AkCcuwmpw9JJSX26zi


Generated by Claude Code

@cacheable/net previously threw `Error("Fetch failed with status …")` on any
non-2xx response, which diverged from native `fetch` semantics. Callers written
for `fetch` check `response.ok`, but that became unreachable dead code because the
request already rejected, producing cryptic errors on non-2xx (e.g. routing a
subscription POST through the client).

Align with native `fetch`:
- Resolve with the Response for any completed exchange (4xx/5xx included); only
  reject on real network errors. Removes all four throw sites.
- Only cache storable responses: simple mode caches 2xx only; HTTP-cache mode
  continues to defer to RFC 7234 `storable()`. Errors are returned, never cached.
- Make `options` optional so `fetch(url)` works, matching `fetch(input, init?)`
  and the documented signature.
- Preserve `response.url` (plus `redirected`/`type` from live responses) on
  reconstructed/cached responses via a shared `makeResponse` helper, so the final
  URL survives caching and the get/post/etc. helpers.

Add deterministic local-server tests covering non-2xx across every path (no
cache, simple cache, HTTP cache, POST/HEAD, helpers, CacheableNet), error
responses not being cached, optionless `fetch(url)`, and url/redirected
preservation. Document the native-fetch error semantics in the README.

https://claude.ai/code/session_01QeG7AkCcuwmpw9JJSX26zi
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request aligns @cacheable/net with native fetch semantics by resolving completed HTTP exchanges (including 4xx and 5xx statuses) instead of throwing errors. It introduces a makeResponse helper to reconstruct responses while preserving native properties like url, redirected, and type. Additionally, the documentation has been updated to reflect these error handling changes, and comprehensive tests have been added to verify native fetch parity. There are no review comments, and I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ee73e5a) to head (961093d).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #1652   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           26        26           
  Lines         2705      2725   +20     
  Branches       598       607    +9     
=========================================
+ Hits          2705      2725   +20     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9083af5614

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/net/src/fetch.ts Outdated
Comment thread packages/net/src/fetch.ts Outdated
Comment thread packages/net/src/fetch.ts
Comment thread packages/net/src/fetch.ts Outdated
claude and others added 3 commits June 4, 2026 16:53
…-redirect metadata

Resolves four issues raised in review of the native-fetch alignment:

- Don't cache HTTP errors under default cache semantics. Non-2xx responses now
  return early before policy creation, so statuses RFC 7234 deems storable
  (404/410/501/…) are no longer served from cache — matching the documented
  "errors are never cached" contract.
- Fix a 304/null-body throw. Reconstructing `new Response("", { status: 304 })`
  threw, so conditional GETs (and 204s) still rejected. makeResponse now coerces
  the body to null for null-body statuses (101/103/204/205/304).
- Restore stampede protection in simple-cache mode. The get+fetch+set rewrite
  dropped getOrSet's coalescing; concurrent misses now share a single origin
  request via coalesceAsync, while each caller rebuilds its own Response (so the
  body stays independently readable) and errors remain uncached.
- Persist final-URL metadata for cached responses. CachedResponse now stores
  url/redirected/type so cache hits report the same final URL (after redirects)
  as the original miss, instead of falling back to the request URL.

Adds local-server tests for each (304 helper, concurrent-miss coalescing,
cached-redirect metadata, and request-url fallback for legacy entries) and
updates the README error-handling note. Adds @cacheable/utils dependency for
coalesceAsync.

https://claude.ai/code/session_01QeG7AkCcuwmpw9JJSX26zi
Update the lead feature bullet, which still described fetch as coming from
undici with caching always on. Reflect the current behavior: a drop-in fetch
built on the runtime's global fetch that resolves on any status (check
response.ok), preserves response.url/redirected/type, with caching opt-in.

https://claude.ai/code/session_01QeG7AkCcuwmpw9JJSX26zi
@jaredwray jaredwray merged commit c5eb62c into main Jun 4, 2026
12 checks passed
@jaredwray jaredwray deleted the claude/blissful-dijkstra-WESGR branch June 4, 2026 17:39
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.

2 participants