Skip to content

writeH1 calls socket.setTypeOfService() unconditionally — uncaught EINVAL on macOS crashes the process #5544

Closed

Description

@quolu

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:

  1. Wrap the setTypeOfService call in try/catch (ignore or debug-log failures), and/or
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions