Skip to content

surface the client's websocket close code and reason - #46

Merged
bentsku merged 1 commit into
mainfrom
aws-1924-rolo-surface-the-clients-websocket-close-code-and-reason
Sep 30, 2026
Merged

bentsku merged 1 commit into
mainfrom
aws-1924-rolo-surface-the-clients-websocket-close-code-and-reason

Conversation

@bentsku

@bentsku bentsku commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Motivation

When a client closes a WebSocket with a code and reason (e.g. 4001 / "test reason"), the application served through the twisted serving layer never sees them. ws.receive() raises WebSocketDisconnectedError with the default code 1005 and no reason:

  • WebSocketChannel.dataReceived calls self.close() on the client's CloseConnection event before queueing it. close() queues its internal poison pill CloseConnection(None), so the consumer reads the pill first (→ 1005) and the client's real close event is never read.
  • WebSocketDisconnectedError only carries the code, so the reason is dropped even when the event is read.

In LocalStack, API Gateway WebSocket APIs pass the close code and reason to the $disconnect route (requestContext.disconnectStatusCode / disconnectReason), which we currently can't populate.

Changes

  • WebSocketChannel.dataReceived queues the event before completing the closing handshake, so the client's CloseConnection comes ahead of the poison pill.
  • WebSocketDisconnectedError accepts an optional reason (exposed as .reason, defaults to None, so this is backwards compatible).
  • The twisted adapter passes event.reason. The ASGI adapter passes event.get("reason") (the optional reason key on websocket.disconnect in the ASGI spec).

Testing

  • New test_close_code_and_reason_client_initiated: the client closes with 4001 / "test reason", and the server-side exception must carry both. It fails without the fix (assert 1005 == 4001).
    • It runs against twisted only: hypercorn (our ASGI test server) doesn't forward the client's close code or reason to the app, and always reports 1000 / 1006 without a reason.
  • Full test suite passes (169 passed), lint clean.

🤖 Generated with Claude Code

The twisted WebSocketChannel queued the client's CloseConnection event only
after close() had queued its internal poison pill, so consumers always saw
code 1005. Queue the event first, and carry the close reason in
WebSocketDisconnectedError (twisted and ASGI adapters).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bentsku
bentsku merged commit cfe2010 into main Sep 30, 2026
5 checks passed
@bentsku
bentsku deleted the aws-1924-rolo-surface-the-clients-websocket-close-code-and-reason branch September 30, 2026 13:19
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