Skip to content

Commit c5eb62c

Browse files
jaredwrayclaude
andauthored
fix(net): make fetch behave like native fetch (#1652)
* fix(net): make fetch behave like native fetch @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 * fix(net): address review — no error caching, coalescing, 304 + cached-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 * docs(net): describe native fetch semantics in README features 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ee73e5a commit c5eb62c

7 files changed

Lines changed: 641 additions & 117 deletions

File tree

packages/net/README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111

1212
Features:
13-
* `fetch` from [undici](https://github.com/nodejs/undici) with caching enabled via `cacheable`
13+
* Drop-in `fetch` with native semantics (built on the runtime's global `fetch`) — resolves with a `Response` on any status (check `response.ok`, no throwing on `4xx`/`5xx`) and preserves `response.url`, `redirected`, and `type`
14+
* Optional response caching via `cacheable` — pass a cache instance (or use `CacheableNet`) to enable it
1415
* HTTP method helpers: `get`, `post`, `put`, `patch`, `delete`, and `head` for easier development
1516
* [RFC 7234](http://httpwg.org/specs/rfc7234.html) compliant HTTP caching with `http-cache-semantics`
1617
* Smart caching with automatic cache key generation
@@ -119,6 +120,29 @@ const result2 = await net.post('https://api.example.com/data', { value: 1 }, {
119120
});
120121
```
121122

123+
## Error Handling
124+
125+
`@cacheable/net` follows native `fetch` semantics. It **resolves** with a `Response` for
126+
every completed HTTP exchange — including `4xx` and `5xx` — and only rejects when the request
127+
itself fails (DNS failure, connection refused, abort, etc.). Use `response.ok` (or
128+
`response.status`) to detect HTTP errors instead of a `try/catch`:
129+
130+
```javascript
131+
const net = new CacheableNet();
132+
133+
const { response, data } = await net.get('https://api.example.com/thing');
134+
if (!response.ok) {
135+
// 404, 500, etc. — `data` holds any error body the server returned
136+
throw new Error(`Request failed with status ${response.status}`);
137+
}
138+
```
139+
140+
Only successful responses are cached. Under the default HTTP cache mode, `2xx` responses are
141+
cached per RFC 7234 (honoring `Cache-Control`, `ETag`, `Expires`, etc.); in simple mode
142+
(`httpCachePolicy: false`) every `2xx` response is cached. Error responses (`4xx`/`5xx`) are
143+
always returned to the caller but **never** cached, so a transient failure is never replayed
144+
from a cache hit.
145+
122146
## API Reference
123147

124148
### CacheableNet Class

packages/net/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"typescript": "^5.9.3"
4141
},
4242
"dependencies": {
43+
"@cacheable/utils": "workspace:^",
4344
"cacheable": "workspace:^",
4445
"hookified": "^1.15.1",
4546
"http-cache-semantics": "^4.2.0",

0 commit comments

Comments
 (0)