fix: report the close frame as the disconnect reason - #92
Open
nmbrone wants to merge 1 commit into
Open
Conversation
nmbrone
force-pushed
the
fix/close-reason
branch
from
September 3, 2026 17:21
4f00b0f to
16304a0
Compare
nmbrone
force-pushed
the
fix/close-reason
branch
5 times, most recently
from
September 3, 2026 17:47
b877353 to
06f3a93
Compare
The close handshake is handled internally, so the code and reason the server
sent were dropped: `handle_disconnect/3` only ever saw the transport error from
the socket shutting down afterwards, leaving application close codes such as
1008 or the 4000 range unreachable.
Remember the frame when the handshake starts and report it instead, falling
back to the Mint error when there was no handshake. The frame is cleared on
close so a code cannot leak into the next connection's disconnect.
Add a `:close_timeout` option to make the handshake timeout configurable, and
cover it with a test that suspends the server so the handshake goes unanswered.
`handle_disconnect/3` now receives `{:close, code, reason}` where it previously
received `%Mint.TransportError{reason: :closed}` for a connection closed through
the handshake by either side.
fix: cancel the previous close timer when starting a handshake
`send_close/2` overwrote `close_timer` without cancelling it, and `close/1` only
cancels the timer it can see. Two handshakes inside the timeout window left the
first timer armed, so it fired after the connection had been closed and
re-established, and tore down the healthy connection.
nmbrone
force-pushed
the
fix/close-reason
branch
from
September 3, 2026 17:48
06f3a93 to
62b679d
Compare
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.
Three connected changes to the close handshake.
The close code was unreachable. The handshake is handled internally, so the code and reason the server sent were dropped —
handle_disconnect/3only ever saw the transport error from the socket shutting down afterwards. Application close codes (1008, 1011, the 4000 range) never reached the client, which is exactly what it needs to decide whether reconnecting makes sense.send_close/2now remembers the frame and reports it as the reason, falling back to the Mint error when there was no handshake;close/1clears it so a code can't leak into the next connection's disconnect.The handshake timeout is now configurable via
:close_timeoutonstart_link/3(default5000), which also makes it testable — the new test suspends the server process so our close frame is never read and the socket never closes, which is the situation that timeout exists for.A second handshake orphaned the first timer.
send_close/2overwroteclose_timerwithout cancelling it, andclose/1only cancels the timer it can see, so the stale one fired after the connection had been re-established and tore it down. That path is also what made the timeout's reason ambiguous, so it belongs with the other two.The frame is reported the same way whichever side initiated the close: per the protocol the responding endpoint echoes the status code, so the code converges anyway, and for a local close the echo is never decoded (
send_close/2has already nil'd the websocket).handle_disconnect/3now gets{:close, code, reason}where it previously got%Mint.TransportError{reason: :closed}. Not flagged as breaking: the reason is typedterm()and was undocumented, and the old value was the same for a graceful close and for a socket that simply vanished, so there was nothing dependable to match on. Two existing tests updated.