fix(server): serialize async websocket messages
Fix #32483
Summary
- pause the underlying uWS websocket while an async
websocket.messagehandler is pending, so two messages on the same socket cannot run their handlers concurrently - resume reads when the handler promise settles — on rejection as well as resolution
- route rejected async message handlers through the existing
websocket.errorcallback - skip the
pause()when the handler closed or terminated the socket synchronously before suspending (the socket is already gone;AnyWebSocket::pause()would deref it) - bump
promiseFunctionsSizeto cover the two newPromiseFunctionsentries, with astatic_assertso the array can never again be sized below the highest enum index
Testing
Serialization regression:
USE_SYSTEM_BUN=1 bun test test/js/bun/http/bun-server.test.ts -t "async websocket message handlers run serially per socket"
Fails on released Bun with maxInFlightHandlers: 2 and secondStartedBeforeFirstSettled: true.
Reject-path regression:
USE_SYSTEM_BUN=1 bun test test/js/bun/http/bun-server.test.ts -t "rejected async websocket message handler"
Fails on released Bun — the rejection escapes to the uncaught path on stderr instead of reaching websocket.error, and the socket never resumes, so the follow-up message is never processed:
- "echoed": "done:after-reject", + "echoed": null,
- "errorMessage": "async websocket ... + "errorMessage": null,
+ stderr: "error: async websocket handler rejected"
0 pass 1 fail
Both pass with the fix:
bun bd test test/js/bun/http/bun-server.test.ts -t "async websocket"
# 2 pass 0 fail 4 expect() calls
Full file plus bun test test/internal/source-lints/ are green, with one pre-existing exception: Server > normlizes incoming request URLs (an HTTPS fetch test unrelated to this diff) takes 4.42s standalone against the 5s default timeout and tips over under suite load in a debug build. It passes in isolation on the same binary.
Notes
The is_closed() guard on pause() has no dedicated test: the unguarded path does not fault deterministically in a non-ASAN build, and a "does not crash" test would never fail in CI. close() and terminate() both set flags.closed before touching uWS, so is_closed() is accurate at that point; the guard matches what on_drain and on_message_promise_resolve already do for their uWS calls.