fix(adapter): read the rest of a body its answer left unread, or close the connection

Merged
K-
2h ago
+406-3

Summary

  • The native server gave the runtime Readable.toWeb(req) as the request body. When the application stopped reading a body, the request was left paused, or destroyed with its connection detached. Either way, Node.js stopped reading the connection.
    • This happened to a Server Action over its size limit, to a middleware's side of a split body that nothing read, and to a POST to a static page.
    • The connection then answered nothing more until its keep-alive timeout.
  • The body is now read from the request directly.
    • Cancelling the body stops the reading and leaves the request alone.
    • Once the answer has gone out, the rest is read and dropped within workerd's grace: 64 KiB and 1 s, from KJ's HttpServerSettings.
    • Past either limit, the connection is ended, then dropped 500 ms later so the client can read its answer first.
    • A read of the body after its answer fails, as it does in workerd.

How it showed up

In the 16.4.3 deploy-tests nightly (run 41491299214), app-action-size-limit-invalid and its node-middleware variant timed out on node, once on Node.js 24 and once on 26.

  • The browser's load of /form after the 3 MB "exceeds the limit" action never completed.
  • deploy-tests runs upwind start --keepAliveTimeout 600000 (#434), so the browser's next request on that connection waited for the 10-minute timeout.
  • Before #434, Node.js's default closed the connection after about 6 s, and the browser retried.

Verification

  • The new native-request-body.test.ts has 6 cases. 4 of them fail on main: the connection is not closed after the answer, a slow remainder is not bounded, a cancelled body leaves the connection stuck, and a late read succeeds.

  • The upstream suite's sequence (the 3 MB plaintext action, then /form) was driven with Playwright against a local build. All runs used a keep-alive timeout of 10 minutes:

    ServerPage loads that hungConnections left
    upwind start, main1 of 6stuck ones
    upwind start, this PR0 of 20none
    next start0 of 8—
  • The tests ran on Node.js 24.22.1 and 26.13.0.

Not in this PR

The private bridge between the runtime and Node.js handlers leaves the same kind of connection behind.

  • On the server side, those connections have never been closed since #432.
  • A pooled one can hold the next call until the bridge's 30 s deadline, which answers 504.

That is #440.