Skip to content

fix(server): stop malformed WebSocket frames from crashing the process - #2108

Merged
dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/websocket-error-handling-caaf6d
Sep 28, 2026
Merged

dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/websocket-error-handling-caaf6d

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 28, 2026

Copy link
Copy Markdown
Member

Any WebSocket client could crash a Node server that uses the ws library. ws throws when an error event has no listener, and WebSocketHandler.upgrade() only listened for message and close, so one malformed frame (invalid UTF-8, unexpected RSV bits, ...) took the whole process down. upgrade() now absorbs the error event: the offending connection is closed with the proper status code and the server keeps running.

Fixes

  • A malformed frame from a client now closes only that connection (e.g. code 1007 for invalid UTF-8) instead of throwing an uncaught error that kills the process.
  • In-flight procedures on that connection are still aborted by the existing close cleanup.
  • Deno, Bun, Cloudflare and browser sockets behave as before.

Testing

  • New regression test sends an invalid UTF-8 text frame through a real ws server and client. Without the fix, vitest reports the uncaught error and exits 1.
  • ws is now a devDependency of @orpc/server; its types come from the root, like supertest.

Follow-ups

  • v1's ws and websocket adapters have the same gap and need a backport to 1.x.
  • The client RPCLink over ws has the same gap (a refused connection crashes the client) and is being fixed separately.

`ws` sockets are EventEmitters and throw when an `error` event has no
listener, so a single malformed frame from any client (invalid UTF-8,
unexpected RSV bits, ...) crashed the Node process. `upgrade()` now
absorbs `error`; `ws` still closes the connection and the existing
`close` listener handles cleanup.
@codecov

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pkg-pr-new

pkg-pr-new Bot commented Sep 28, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@2108

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@2108

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@2108

@orpc/client

npm i https://pkg.pr.new/@orpc/client@2108

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@2108

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@2108

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@2108

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@2108

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@2108

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@2108

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2108

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@2108

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@2108

@orpc/next

npm i https://pkg.pr.new/@orpc/next@2108

@orpc/node

npm i https://pkg.pr.new/@orpc/node@2108

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@2108

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@2108

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@2108

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@2108

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@2108

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@2108

@orpc/server

npm i https://pkg.pr.new/@orpc/server@2108

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@2108

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@2108

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@2108

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@2108

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@2108

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@2108

commit: 6598265

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Absorb error events on upgraded sockets — WebSocketHandler.upgrade() now attaches a no-op error listener alongside message/close, so an EventEmitter-based transport such as ws no longer throws an uncaught error (and crashes the process) when a client sends a malformed frame. close still drives the existing peer cleanup.
  • Regression test — spins up a real ws server/client, sends an invalid UTF-8 text frame, and asserts the connection closes with code 1007.
  • ws devDependency — added at ^8.21.3 to @orpc/server, with the matching lockfile importer entry.

I verified the fix and the test locally: with the change the new test passes; reverting the listener leaves the file at 10/10 passing but vitest records the unhandled WS_ERR_INVALID_UTF8 and exits 1, so the regression signal is real. pnpm --filter @orpc/server type:check and eslint on both changed files are clean, and the lockfile already contained the ws@8.21.3 package snapshot. The no-op listener is harmless on DOM, Bun, Cloudflare, and MessagePort sockets, so no behavior change there.

The silent swallow is consistent with the server package, which does no logging anywhere; the v1 backport and the client RPCLink gap are already acknowledged as follow-ups in the PR description.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@codspeed

codspeed Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing dinwwwh:claude/websocket-error-handling-caaf6d (6598265) with main (6e2bf7e)

Open in CodSpeed

@dinwwwh
dinwwwh merged commit dfa4e1e into middleapi:main Sep 28, 2026
11 checks passed
dinwwwh added a commit that referenced this pull request Sep 28, 2026
…nk (#2111)

A client using the WebSocket `RPCLink` with Node's `ws` library no
longer crashes when the socket emits `error`. `ws` throws when `error`
has no listener, and the link transport only listened for `open`,
`message` and `close`, so a refused connection or a malformed frame from
the server took the whole client process down. The transport now absorbs
`error`, and the `close` event that always follows rejects pending calls
or drives reconnection. This is the client-side counterpart to #2108.

## Fixes

- A refused connection rejects the call with `WebSocket closed (code
1006: )` instead of crashing and leaving the call hanging.
- With `reconnect` enabled, refused attempts while the server is down go
through the normal retry path (including `maxAttempt` and proactive
`onClose` reconnects) instead of crashing on every attempt.
- A malformed frame from the server rejects pending calls instead of
throwing an uncaught `RangeError`.
- Browser, Deno, Bun and Cloudflare sockets behave as before.

## Testing

- New regression tests with a real `ws` server and client cover a
refused connection, reconnecting after one, and a malformed server
frame. Run alone without the fix, each exits 1 on the uncaught error.
- `ws` and `@types/ws` are now devDependencies of `@orpc/client`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant