Skip to content

Quiet websocket protocol errors found after deserialization - #746

Merged
mtrudel merged 1 commit into
mtrudel:mainfrom
neilberkman:ws-protocol-error-shutdown
Nov 11, 2026
Merged

mtrudel merged 1 commit into
mtrudel:mainfrom
neilberkman:ws-protocol-error-shutdown

Conversation

@neilberkman

Copy link
Copy Markdown
Contributor

A frame that deserializes correctly can still violate the protocol: a text frame that is not valid UTF-8, a continuation frame with no message in progress, a data frame in the middle of a fragmented message, a compressed frame when compression was not negotiated or that fails to inflate, or a fragmented message over the configured limit. Bandit.WebSocket.Connection returned these from handle_frame/3 as {:error, reason, connection} with the bare string as the reason, so the handler process stopped abnormally. Each one produced a GenServer crash report containing the whole handler state, the WebSock state included, and websocket_options: [log_protocol_errors: false] did not apply to them. #610 fixed this for errors found while deserializing a frame; these are the ones found afterwards.

This change adds do_protocol_error/4 next to do_error/4 in Bandit.WebSocket.Connection. It calls do_error/4 unchanged, logs through Bandit.Logger.maybe_log_websocket_protocol_error/2, and returns {:error, {:shutdown, reason}, connection}. The ten call sites where the peer caused the error use it. Errors returned or raised by the WebSock callbacks, and the deflation error on an outgoing message, still go through do_error/4 and behave as before, crash report included.

The close code sent to the client, the reason passed to terminate/2, and the error value in the [:bandit, :websocket, :stop] event are unchanged. The connection process exit reason changes from the string to {:shutdown, string}. The error in [:thousand_island, :connection, :stop] is also unchanged by default; with silent_terminate_on_error: true it becomes {:shutdown, string}, as it already is for deserialization errors.

Nine tests in the "protocol error logging" block of test/bandit/websocket/protocol_test.exs: invalid UTF-8, unexpected continuation and unexpected compressed frame, each with log_protocol_errors set to :short, :verbose and false. Each asserts the close frame, the terminate/2 reason, a {:shutdown, reason} exit with no crash report, and one log line or none. The existing tests that exercise these errors now also assert that no crash report is logged for them, which covers all ten call sites; fourteen existing tests fail without the change.

Verified with mix format --check-formatted, mix compile --warnings-as-errors, mix credo --strict, mix dialyzer, and mix test on Elixir 1.20.4 / OTP 29 (1083 passed, 1 skipped, 8 excluded; the 3 failures are in HibernationTest and also fail on main under OTP 29, where a hibernating process reports {:gen_server, :loop_hibernate, 4}).

Frames that deserialize correctly can still violate the protocol: a
text frame that is not valid UTF-8, a continuation frame with no
message in progress, a data frame in the middle of a fragmented
message, a compressed frame when compression was not negotiated or
that fails to inflate, or a fragmented message over the configured
limit. These stopped the connection process with the bare error string
as its reason, so each one produced a GenServer crash report holding
the full connection state, WebSock state included, and the
log_protocol_errors websocket option did not apply to them.

Handle them the way frame deserialization errors are handled since
mtrudel#610: send the close frame and terminate the websock as before, log a
one-line summary governed by log_protocol_errors, and exit with a
{:shutdown, reason} reason so no crash report is emitted. The close
code, the reason given to terminate/2 and the websocket telemetry stop
event are unchanged. Errors returned or raised by the websock itself
keep their current behaviour.
@mtrudel

mtrudel commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Looks good!

@mtrudel
mtrudel merged commit 0248cfe into mtrudel:main Nov 11, 2026
25 of 29 checks passed
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.

2 participants