Skip to content

fix(ext/fetch): reject transport failures with Node's "fetch failed" shape - #35618

Merged
bartlomieju merged 3 commits into
mainfrom
fix/fetch-failed-error-shape-35612
Jul 6, 2026
Merged

bartlomieju merged 3 commits into
mainfrom
fix/fetch-failed-error-shape-35612

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

Closes #35612

Problem

When the server closes the connection mid-response (or any other transport-level failure occurs), Deno rejected fetch with the raw reqwest/hyper detail directly in .message and left .cause undefined:

msg="error sending request from 127.0.0.1:.. for http://.. (..): client error (SendRequest): connection closed before message completed" | cause=undefined

Node/undici instead reject with TypeError: "fetch failed" and put the detail in .cause:

msg="fetch failed" | cause=other side closed

Node-compatible error handling branches on this shape (err.message === "fetch failed" / inspecting err.cause), so the divergence breaks libraries — surfaced while running the fastify suite under deno test (fastify/fastify#6804).

Change

op_fetch_send now remaps ClientSend transport errors (connection refused, DNS failure, connection reset, connection closed mid-response, request body stream errors, …) to Node's shape: a TypeError whose message is "fetch failed" and whose .cause carries the low-level detail. Permission errors and other typed errors keep their own type. The detail is still emitted to the inspector Network domain (Network.loadingFailed.errorText) and to OpenTelemetry (exception.message).

After the fix, the issue's reproduction matches Node:

msg="fetch failed" | cause=error sending request from .. (..): client error (SendRequest): connection closed before message completed

Tests

  • New regression test fetchConnectionClosedBeforeMessageComplete mirroring the issue reproduction.
  • Updated existing fetch unit tests and spec fixtures (otel_basic, fetch_async_error_stack) to assert the Node-compatible shape (detail moved from .message to .cause).

Note on scope

This covers the request/response phase ("fetch failed"). A failure while reading the response body after headers are received is a separate Node shape (TypeError: "terminated") and is left for a follow-up.

…shape

When a fetch transport failure occurs (connection refused, DNS failure,
connection reset, connection closed mid-response, request body stream
errors, ...), Deno put the low-level reqwest/hyper detail directly in the
rejected error's `.message` and left `.cause` undefined. Node/undici
instead reject with `TypeError: "fetch failed"` and surface the detail
via `.cause`, which is the shape libraries branch on (e.g. checks for
`err.message === "fetch failed"` / inspecting `err.cause`).

Remap `ClientSend` errors in `op_fetch_send` to that shape. Permission
and other typed errors keep their own type. The detail is still emitted
to the inspector Network domain and OTel `exception.message`.

Closes #35612
…test

- cert::untrusted_cert_hint failed because the TLS hint matcher in
  fmt_errors.rs only inspects the top-level error message; the detail now
  lives in the cause of "TypeError: fetch failed". Fall back to the cause
  chain when the top-level message yields no suggestions.
- serve_test's "Throw if disturbed"/"Throw if locked" asserted the raw
  transport error message on the client fetch; update to the new shape
  (message "fetch failed", detail in .cause).
@bartlomieju

Copy link
Copy Markdown
Member Author

CI failures addressed in a9d1559 (merged latest main in 2891071 along the way):

  • specs::cert::untrusted_cert_hint (all platforms): the DENO_TLS_CA_STORE hint matcher in runtime/fmt_errors.rs only inspected the top-level error message, and the TLS detail now lives in the cause of TypeError: fetch failed. get_message_suggestions now falls back to walking the cause chain when the top-level message yields no suggestions — this keeps the hint working for any error that wraps its actionable detail in .cause.
  • unit::serve_test "Throw if disturbed" / "Throw if locked" (all platforms): these asserted the raw transport message (message.includes("client error")) on the client-side fetch rejection; updated to the new shape (message === "fetch failed", detail in .cause), matching the assertions in fetch_test.ts.

@bartlomieju
bartlomieju merged commit 4a08965 into main Jul 6, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the fix/fetch-failed-error-shape-35612 branch July 6, 2026 09:28
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.

fetch: connection closed mid-response should reject with "fetch failed" + .cause, not a raw reqwest message

1 participant