Skip to content

undici request() and ws server both throw 'Cannot read properties of undefined' at the transport layer despite compiling cleanly #10797

Description

@proggeramlug

Two HTTP/WebSocket packages compile from real source but fail at runtime, both before any network I/O completes. Filing together because both are transport-layer failures found in the same probe and may share a cause; split if they diverge.

Found on origin/main (v0.5.1617), --profile perry-dev, Linux x64, versions pinned to each binding's [bindings.<pkg>.upstream] block.

undici 8.9.0 — 112 modules, compiles, then throws before connecting

TypeError: Cannot read properties of undefined (reading 'length')

Thrown from request() before any TCP connect is attempted, so it is not a socket or protocol problem — something is undefined in the request-construction path.

ws 8.21.1 — handshake provably correct, crash immediately after

TypeError: Cannot read properties of undefined (reading 'destroy')

This one is unusually well-localised by the probe: the WebSocket handshake is byte-exact, with a correct RFC 6455 Sec-WebSocket-Accept value computed and sent. The server then crashes before the 'connection' event fires. So the protocol logic works and the failure is in what happens to the socket object immediately afterwards — something that should be a socket is undefined at the point .destroy() is called on it.

Why these are worth taking together

Both are Cannot read properties of undefined on an object that the package expects the runtime to have provided — a socket in ws's case, something in the request path for undici. Both packages sit directly on node:net/node:http internals rather than on the public surface. If Perry is under-populating an object that Node provides, one fix may resolve both. If they turn out to be unrelated, this should be split into two issues.

Reproduction

Both: install the pinned version, add a perry.compilePackages entry, and exercise the primary documented use — request() against a local server for undici; a WebSocketServer plus a client connection for ws. Compare against node --experimental-strip-types on Node 26.5.1.

Note on how these were found

Both packages passed a static pre-screen for every known blocker shape (export = + namespace, conditional require, new Function with class expressions, bound-export native heritage, require.main). They failed anyway. That is the screen working as designed — it narrows where to look and predicts nothing about what it has not seen before — and it is why compiling is not optional before declaring a binding removable.

Activity

  1. added
    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
    on Sep 20, 2026
  2. proggeramlug commented on Sep 20, 2026

    @proggeramlug
    ContributorAuthor

    Re-examine these two before investing in them — two of the four blockers from the same probe wave have now turned out not to be what they were reported as.

    What that implies for this issue

    Both false readings share a cause: the probe exercised these packages in ways that differ from how a program actually consumes them — routing around a hard failure with a partial-codegen flag in one case, reaching into package internals in the other. Neither symptom was a lie; both were real observations of something other than the stated defect.

    So before anyone spends a day on undici or ws:

    1. Reproduce each from a plain, ordinary consumer — a normal import/require of the package's public entry point, no partial-codegen flags, no reaching into internals, pinned to the version in [bindings.<pkg>.upstream].
    2. Check perry.compilePackages: a compiled package's internal file, required from OUTSIDE the package, exports a class as typeof object (not function) — new X() throws 'is not a constructor' #10806 first for ws. Its reported failure is Cannot read properties of undefined (reading 'destroy') immediately after a byte-correct handshake — and ws is exactly the kind of package where a server implementation reaches across internal module boundaries. If the probe's fixture touched internals, perry.compilePackages: a compiled package's internal file, required from OUTSIDE the package, exports a class as typeof object (not function) — new X() throws 'is not a constructor' #10806 could produce a symptom like this.
    3. Check Cross-module inlining copies Enum.Member references into the importer: "enum member X.Y not found in enums table" #10417 for both, as cheerio's case showed a hard compile failure can be routed around into a misleading runtime error.

    The underlying defects may well be real — undici's failure before any TCP connect, and ws's correct handshake followed by an undefined socket, are both specific and plausible. This is not a claim that they are artifacts. It is that two of four from this batch were, so the reproduction should be re-established from a clean consumer before the investigation is scoped.

    Method note worth keeping

    A probe that reports "package X fails with error E" is reporting what it observed, not necessarily what a user would hit. The observation is still valuable — it is what surfaced #10806 and #10417's reach — but the framing "this package is blocked by E" carries an inference the probe did not test. Recording the fixture alongside the error, not just the error, would have made both of these visible immediately.

  3. proggeramlug commented on Sep 21, 2026

    @proggeramlug
    ContributorAuthor

    Re-established both probes from clean public-entry consumers on current main (47ade47327), with the package versions from this issue and no partial-codegen flags or internal-file imports. Neither reported exception reproduces.

    ws@8.21.1

    Fixture imports { WebSocketServer, WebSocket } from ws, starts a local server, and connects a public client. It compiles and runs, but now stops at:

    Error: Unexpected server response: 426
    

    The original post-handshake undefined.destroy crash does not occur.

    undici@8.9.0

    Fixture imports { request } from undici and requests a local node:http server. The clean 111-module source graph reaches link, then is blocked by the llhttp WebAssembly dependency:

    undefined reference to js_webassembly_module_new
    

    The original pre-connect undefined.length runtime failure therefore is not currently reachable from the supported public consumer path.

    These are distinct current blockers, not evidence for the transport-object hypothesis in the issue, and they should be split/reframed before code changes are proposed. I am not opening a speculative/no-op PR for #10797.

  4. proggeramlug commented on Sep 21, 2026

    @proggeramlug
    ContributorAuthor

    Closing this combined report because neither stated undefined failure reproduces from clean public-entry consumers on current main. The current, distinct blockers are now tracked separately as #10888 (ws, HTTP 426) and #10889 (undici, missing WebAssembly runtime symbol).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions