Transport conformance
Each behaviour below runs the same test code on every adapter rust-fetch has: both Node.js transports, fetch() in Node.js (the browser build without the Node.js entry), and real browsers. ✓ is the expected behaviour; "differs" is a documented difference, explained under the table. Generated by npm run test:conformance (wasm-module/js/test/conformance/), which fails CI on any other difference. The Rust client is covered by its own integration tests.
Redirects
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| GET through a 301 is followed | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| GET through a 302 is followed | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| GET through a 303 is followed | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| GET through a 307 is followed | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| GET through a 308 is followed | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| POST through a 301 becomes GET, without a body | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| POST through a 302 becomes GET, without a body | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| POST through a 303 becomes GET, without a body | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| POST through a 307 becomes POST with its body | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| POST through a 308 becomes POST with its body | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| PUT through a 303 becomes GET without a body | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Authorization is dropped on a cross-origin redirect, kept on a same-origin one | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| sensitiveHeaders are dropped on a cross-origin redirect | ✓ | ✓ | differs (1) | differs (1) | differs (1) | differs (1) |
| maxRedirects: 2 stops a third redirect | ✓ | ✓ | differs (2) | differs (2) | differs (2) | differs (2) |
| A redirect loop fails | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| redirect: 'manual' returns the 3xx | ✓ | ✓ | ✓ | differs (3) | differs (3) | differs (3) |
| redirect: 'error' rejects a redirect | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| A redirect to another scheme (data:) is refused | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Decompression
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| gzip is decoded | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| deflate is decoded | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| br is decoded | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| A corrupt gzip body fails | ✓ | ✓ | ✓ | ✓ | ✓ | differs (4) |
| A 64 MB gzip bomb stops at maxResponseBytes | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Size limits
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| maxResponseBytes: exactly at the limit passes, one byte more fails | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| maxResponseBytes errors a streamed body once it passes the limit | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Timeouts and cancellation
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| A timeout before the headers is a TimeoutError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| The timeout covers reading the body | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| An already-aborted signal rejects without sending | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Aborting while waiting for the response is a CancelledError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Aborting mid-stream errors the body with a CancelledError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Errors
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| A refused connection is a NetworkError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| A 404 is an HttpError with the status | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Malformed JSON falls back to text by default | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Malformed JSON with responseType: 'json' is a ParseError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| CR/LF in a header value is an InvalidInputError, before sending | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| A 503 is retried, and the retry succeeds | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Streaming
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| A streamed body arrives in full | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| A connection dropped mid-body errors the stream with a NetworkError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| A slow reader of a streamed body holds the server back, and cancel() closes the connection | ✓ | ✓ | ✓ | differs (5) | differs (5) | differs (5) |
| A slow reader of NDJSON holds the server back, and cancel() closes the connection | ✓ | ✓ | ✓ | differs (5) | differs (5) | differs (5) |
| Aborting mid-stream closes the connection | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| maxResponseBytes reached mid-stream closes the connection | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| NDJSON: \n and \r\n, empty lines skipped, a last line without a newline | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| maxLineBytes: the lines before a long one, then a NetworkError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Server-Sent Events: types, ids and multi-line data, until [DONE] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| maxEventBytes: the events before a large one, then a NetworkError | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
URLs
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| baseUrl joining, relative paths and query parameters encode the same everywhere | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Security
| Behaviour | node:http | native | fetch() in Node.js | Chromium | Firefox | WebKit |
|---|---|---|---|---|---|---|
| Paths like //host stay under baseUrl | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| allowAbsoluteUrls: false refuses an absolute URL, before sending | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| allowPrivateNetwork: false blocks 127.0.0.1, before sending | ✓ | ✓ | differs (6) | differs (6) | differs (6) | differs (6) |
| maxPendingRequests refuses one more, without sending it | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| retryBudget refuses a retry, keeping the attempt’s error | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Documented differences
- fetch() follows redirects itself, so sensitiveHeaders can't apply (documented)
- fetch() has no redirect-limit option; it applies its own limit (20) instead (documented)
- browsers return an opaque redirect: status 0 and an empty body (documented)
- WebKit decodes responses itself: on macOS a corrupt gzip body fails, but on Linux (libsoup) it's passed through as it arrived, which rust-fetch can't detect
- browsers read a response ahead of the page themselves, however slowly it's consumed: their own fetch() took 59–64 MB of 64 in this test, as rust-fetch did; how far varies by browser and machine
- only the Node.js entry checks addresses; browsers enforce their own private-network rules (documented)