Bug Description
writeH1 calls socket.setTypeOfService(request.typeOfService) unconditionally on every HTTP/1.1 request, with no error handling. Since Request defaults typeOfService to 0 (this.typeOfService = typeOfService ?? 0 in lib/core/request.js), the setsockopt path runs even when the user never asked for any QoS marking. On macOS, Socket.setTypeOfService can throw EINVAL synchronously depending on socket state — and because the call site has no try/catch, the exception is uncaught and kills the whole process.
// lib/dispatcher/client-h1.js (writeH1)if(socket.setTypeOfService){socket.setTypeOfService(request.typeOfService)}Observed crash:
node:net:829
throw new ErrnoException(err, 'setTypeOfService');
^
Error: setTypeOfService EINVAL
at Socket.setTypeOfService (node:net:829:13)
at writeH1 (node:internal/deps/undici/undici:7922:16)
at Object.write (node:internal/deps/undici/undici:7666:18)
at _resume (node:internal/deps/undici/undici:9963:54)
at resume (node:internal/deps/undici/undici:9896:7)
errno: -22, code: 'EINVAL', syscall: 'setTypeOfService'
Reproduced on Node 24.18.0 and 26.4.0 (macOS, Apple Silicon, Darwin 25.5) — the same crash with both bundled undici versions, so LTS is affected too.
Impact
Any app using global fetch can die mid-flight with no way to catch the error (it is thrown from the socket write path, not surfaced through the fetch promise). In our case an MCP (Model Context Protocol) stdio server process silently exited mid-request; the client only saw "connection closed", which made this painful to diagnose.
Reproduction notes
We could not reduce it to a one-liner — plain fetch() against IPv4/IPv6/localhost/https targets succeeds on the same machine. The crash occurs reliably inside a real application (@steipete/oracle 0.15.2, during HTTP calls in its Chrome-automation cleanup phase), so the EINVAL seems to depend on socket state (e.g. connection reuse / socket mid-teardown) rather than address family. Happy to run diagnostics against a debug build if that helps pin it down.
Suggested fix
IP QoS marking is inherently best-effort; a failed setsockopt should never take the process down:
- Wrap the
setTypeOfService call in try/catch (ignore or debug-log failures), and/or - Skip the call entirely when
typeOfService is unset/0, so the default path never touches setsockopt.
Related: this call was introduced with the IP prioritization hints feature (#4831).
Environment
- undici: bundled with Node 24.18.0 and 26.4.0 (also reproduced with the copy bundled in 26.4.0)
- Node.js: 24.18.0, 26.4.0
- OS: macOS (Darwin 25.5.0), arm64
Bug Description
writeH1callssocket.setTypeOfService(request.typeOfService)unconditionally on every HTTP/1.1 request, with no error handling. SinceRequestdefaultstypeOfServiceto0(this.typeOfService = typeOfService ?? 0inlib/core/request.js), the setsockopt path runs even when the user never asked for any QoS marking. On macOS,Socket.setTypeOfServicecan throwEINVALsynchronously depending on socket state — and because the call site has no try/catch, the exception is uncaught and kills the whole process.Observed crash:
Reproduced on Node 24.18.0 and 26.4.0 (macOS, Apple Silicon, Darwin 25.5) — the same crash with both bundled undici versions, so LTS is affected too.
Impact
Any app using global
fetchcan die mid-flight with no way to catch the error (it is thrown from the socket write path, not surfaced through the fetch promise). In our case an MCP (Model Context Protocol) stdio server process silently exited mid-request; the client only saw "connection closed", which made this painful to diagnose.Reproduction notes
We could not reduce it to a one-liner — plain
fetch()against IPv4/IPv6/localhost/https targets succeeds on the same machine. The crash occurs reliably inside a real application (@steipete/oracle0.15.2, during HTTP calls in its Chrome-automation cleanup phase), so the EINVAL seems to depend on socket state (e.g. connection reuse / socket mid-teardown) rather than address family. Happy to run diagnostics against a debug build if that helps pin it down.Suggested fix
IP QoS marking is inherently best-effort; a failed setsockopt should never take the process down:
setTypeOfServicecall in try/catch (ignore or debug-log failures), and/ortypeOfServiceis unset/0, so the default path never touches setsockopt.Related: this call was introduced with the IP prioritization hints feature (#4831).
Environment