fix(ws): stop discarding parser and handler lookup failures silently - #17635
Merged
kamilmysliwiec merged 3 commits intoAug 31, 2026
Merged
Conversation
The three failures that the ws adapter currently swallows had no unit coverage: a throwing custom message parser, a malformed frame, and an event with no registered handler. Adds a case for each, plus the void-parser and happy paths, and one that pins the current behaviour of a throwing handler so the connection teardown risk stays visible. Refs nestjs#17624 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moving it out of the field initialiser gives the adapter a stable reference to compare against, so it can tell whether the parser in use is still the built-in one. Comparing the reference rather than tracking a flag covers every way the parser can be replaced: the constructor option, the setter, and a subclass assigning the protected field directly. Pure refactor, no behaviour change. Refs nestjs#17624 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The message handler binding wrapped its whole body in a single try with an empty catch, so three unrelated failures ended up indistinguishable from a dropped packet: a custom message parser that throws, a malformed frame, and an event with no registered handler. The handler lookup used a non-null assertion followed by a destructure, so an unrecognised event reached that catch as a TypeError. It is now an explicit guard that keeps the single map lookup introduced in 8c7718e. A custom parser that throws is now reported, since nothing else in the process would reveal it. The default parser stays silent because it throws on client-controlled input, and reporting that would turn a public socket into a log flood target. The catch around the handler invocation is kept on purpose: without it a synchronous throw propagates through the mergeMap that binds the client and tears down that client's source stream, silencing every later message from it. Closes nestjs#17624 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
PR Type
What is the current behavior?
Issue Number: #17624
WsAdapter.bindMessageHandlerwraps its whole body in a singletrywith an emptycatch, so three unrelated failures are indistinguishable from a dropped packet — no reply, no log, no exception filter:messageParserthat throws — the application's own bug, invisibleSyntaxErrorhandlersMap.get(...)!returnsundefinedand the destructure on the next line throws aTypeErrorThe
WsProxy/WsExceptionsHandlerpath fixed in #16366 only covers exceptions raised inside a registered handler, which is downstream of all three above.For what it's worth, the empty
catchdoesn't look like a deliberate error policy: it dates back to 53687ba (2018), and the unknown-eventTypeErrorpredates theMap— it was already there when the lookup washandlers.find(...). 8c7718e later swappedfindforgetfor performance and kept the shape as it was.What is the new behavior?
The single
trybecomes three explicit blocks, one per failure origin.Handler lookup.
get(...)!+ destructure is replaced by a plain guard on the result. An unrecognised event now returnsEMPTYbecause the code says so, not because aTypeErrorhappens to be caught. This keeps the single map lookup from 8c7718e — I deliberately did not usehas()+get(), which would double the lookups in the hot path.Parser failures. A custom parser that throws is now reported through
this.logger.error. The default parser stays silent: it throws on client-controlled input, and logging that would turn a public socket into a log flood target — the concern raised in the issue. Telling the two apart is a reference comparison against the extracteddefaultMessageParser, which also covers a subclass assigning the protected field directly.A custom parser that wants a frame dropped quietly can still return nothing —
WsMessageParseris typed=> { event, data } | voidand thevoidcase was already handled. So "throw" can mean "something is wrong" without that being the only way to reject a frame.Unknown events stay silent, as suggested in the issue — dropping unrecognised traffic is a reasonable default for a public socket, and this keeps the change behaviour-preserving there.
One thing worth flagging that isn't in the issue: removing the outer
tryoutright is not safe. A synchronous throw fromcallbackcurrently becomesEMPTY; without atryit propagates through themergeMapinbindMessageHandlersand tears down the client'ssource$, so that client silently stops receiving every subsequent message, not just the failing one. That catch is kept, just separated from the parse one, and there is a test pinning it.Verified end-to-end against a real
wsclient — after an unknown event, a malformed frame, a throwing parser and a throwing handler in sequence, the connection still answers the next valid message.Does this PR introduce a breaking change?
Externally observable behaviour is unchanged in every case except one: a throwing custom parser is now logged. Unknown events, malformed frames on the default parser, and throwing handlers all behave exactly as before.
Other information
Tests live in
packages/platform-ws/test/ws-adapter.spec.ts, extending the file added with thedisposefix. Seven cases: throwing custom parser (constructor andsetMessageParser), malformed frame on the default parser, unknown event, void parser, happy path, and the throwing-handler case that pins the teardown risk above.One open question I did not want to decide unilaterally: should a throwing
callbackalso be logged? I left it at the current behaviour (EMPTY, no log) to keep the diff behaviour-preserving, but it is reachable from client input, so the logging policy there felt like a maintainer call. Happy to add it in a follow-up commit if you want it.