8000 Add close_output for half-close WebSocket support by dominicletz · Pull Request #145 · dominicletz/websockex · GitHub
[go: up one dir, main page]

Skip to content

Add close_output for half-close WebSocket support - #145

Merged
dominicletz merged 7 commits into
masterfrom
cursor/rebase-close-output-141-b57e
Oct 29, 2026
Merged

dominicletz merged 7 commits into
masterfrom
cursor/rebase-close-output-141-b57e

Conversation

@dominicletz
@dominicletz dominicletz commented Oct 22, 2026 •
Copy link
Copy Markdown
Owner

Summary

Adds WebSockex.close_output/1,2,3 so a client can send a close frame and keep reading until the peer finishes the handshake. This is the half-close pattern used by .NET ClientWebSocket.CloseOutputAsync and by Azure Relay Hybrid Connections when a relayed TCP connection shuts down its write side. Rebased onto current master from #141. The HTTP CONNECT proxy work that had landed on this branch is now #148 and is not in this diff.

After close_output:

  • send_frame returns %WebSockex.NotConnectedError{connection_state: :output_closed}
  • incoming frames, including pings, are still handled
  • data-frame replies from callbacks are ignored; pong replies are still sent
  • the peer's close frame completes the handshake and handle_disconnect receives the local close reason
  • a later reconnect clears the half-close flag
  • calling close_output while the socket is still opening returns connection_state: :opening
:ok = WebSockex.close_output(client)
:ok = WebSockex.close_output(client, {1000, "Done sending"})

Tests

Covers send rejection, a custom close code, handshake completion, the local close reason, sending again after reconnect, rejection while opening, and a pong still going out after close_output.

On Elixir 1.18.4 / OTP 27, mix format --check-formatted and mix compile --warnings-as-errors pass. mix test has the same pre-existing failure as master: open_socket rejects a hostname mismatch with supplied cacerts expects {:tls_alert, {:bad_certificate, _}}, and OTP 27 reports {:tls_alert, {:handshake_failure, _}}. The close_output tests pass.

Open in Web Open in Cursor 

Note

Medium Risk
Half-close changes core connection lifecycle and send paths; HTTP CONNECT proxy adds a new TLS setup path for wss without dedicated tests in this PR.

Reviewed by Cursor Bugbot for commit efc4d44. Configure here.

- Implement close_output/2 to send close frame while continuing to receive
- Add output_closed_loop to process incoming frames after output is closed
- Handle callback replies gracefully when output is closed (ignore sends)
- Add error messages for closing and output_closed connection states

Implements #140
Add catch-all {:error, reason} clause to handle errors like
FrameEncodeError when an invalid close code is provided.
This prevents process crashes and returns the error to the caller,
matching the pattern used in sync_send/5.
Reduce code duplication by using a single unified loop with an
output_closed_reason field in state to track the half-close state.
This removes ~160 lines of duplicated code including:
- output_closed_loop (duplicate of websocket_loop)
- handle_frame_output_closed (duplicate of handle_frame)
- handle_fragment_output_closed (duplicate of handle_fragment)
- common_handle_output_closed (duplicate of common_handle)

The unified websocket_loop now handles both normal and output_closed
states by checking the output_closed_reason field.
8000
@cursor cursor Bot left a comment •
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 9 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 9 issues found in the latest run.

  • ✅ Fixed: Reconnect keeps half-close flag
    • reconnect/3 now clears output_closed_reason along with the buffer and fragment, so a new socket is not treated as already half-closed.
  • ✅ Fixed: Proxy TLS omits target hostname
    • The CONNECT tunnel upgrade now sets server_name_indication to the target host unless the caller already supplied it, so verify_peer checks that hostname.
  • ✅ Fixed: close_output return mismatches documented API
    • close_output replies :ok from both the success and already-closed paths, matching the spec and send_frame.
  • ✅ Fixed: Connecting state ignores close_output
    • open_loop now answers $websockex_close_output immediately with NotConnectedError connection_state :opening, so the call does not time out and half-close the later socket.
  • ✅ Fixed: Half-close reports remote disconnect
    • A close frame received after close_output completes the handshake with the stored local reason instead of handle_remote_close.
  • ✅ Fixed: Half-close drops required pong replies
    • After a local close, data-frame replies are still dropped but pong replies are sent until a close is received.
  • ✅ Fixed: Proxy credentials are never sent
    • CONNECT now sends Proxy-Authorization when username/password are set, and proxy URLs keep decoded userinfo.
  • ✅ Fixed: Non-TLS connections ignore proxy
    • ws:// connections with :proxy use the same HTTP CONNECT tunnel and return the tunneled TCP socket without a TLS upgrade.
  • ✅ Fixed: Proxy path drops socket options
    • The proxy TCP connect uses socket_connection_options/1, so caller socket_options such as a bind address are applied.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit efc4d44. Configure here.

Comment thread lib/websockex.ex Resolved
Comment thread lib/websockex/conn.ex Resolved
Comment thread lib/websockex.ex Resolved
Comment thread lib/websockex.ex Resolved
Comment thread lib/websockex.ex Resolved
Comment thread lib/websockex.ex Resolved
Comment thread lib/websockex/conn.ex Resolved
Comment thread lib/websockex/conn.ex Resolved
Comment thread lib/websockex/conn.ex Resolved
cursoragent and others added 3 commits October 29, 2026 21:53
The rebased half-close code failed format --check-formatted on an extra
indent in system_continue/3 and a long NotConnectedError reply.

Co-authored-by: Dominic Letz <dominicletz@users.noreply.github.com>
Clear the half-close flag on reconnect, return :ok from close_output,
reject close_output while opening, and report the local close reason
when the peer completes a half-close. Still send required pongs after
a local close.
A ping made the test server reply with a pong that TestClient does not
handle. Send a binary frame and wait for the server to echo it instead.

Co-authored-by: Dominic Letz <dominicletz@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/rebase-close-output-141-b57e branch from 9aea3df to 8dfc482 Compare October 29, 2026 21:56
@cursor cursor Bot changed the title Rebase close_output half-close support onto master Add close_output for half-close WebSocket support Oct 29, 2026
Cowboy can finish the close handshake before send_frame, so the client
is already :closing. Stall the test server first so the send is rejected
with :output_closed.

Co-authored-by: Dominic Letz <dominicletz@users.noreply.github.com>
@dominicletz
dominicletz merged commit b3a92fc into master Oct 29, 2026
2 checks passed
@dominicletz
dominicletz deleted the cursor/rebase-close-output-141-b57e branch October 29, 2026 22:40
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.

3 participants

0