Skip to content

fix flaky asgi tests and wait for the client's close frame in asgi websockets - #61

Merged
bentsku merged 1 commit into
mainfrom
aws-2208-flaky-tests
Oct 14, 2026
Merged

bentsku merged 1 commit into
mainfrom
aws-2208-flaky-tests

Conversation

@bentsku

@bentsku bentsku commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Two tests failed intermittently, both in the ASGI (hypercorn) variants:

  • tests/test_asgi.py::test_close_iterable_response (failed CI run)
  • tests/websocket/test_websockets.py::test_close_handshake_server_initiated[asgi], which failed occasionally when the whole suite ran

Changes

test_close_iterable_response: race in the test

Once the server has written the last chunk, the iterator is exhausted and the adapter calls close() on the iterable. The client can read bar before or after that happens, so assert not iterable.closed checked a race. A time.sleep(0.2) before that assertion makes it fail every time.

Now the test's iterable waits on an event after its last packet, and the test sets the event once it has checked that the iterable is still open. The final assert iterable.closed was already safe: the adapter closes the iterable before it sends the end of the response body.

test_close_handshake_server_initiated[asgi]: the close handshake didn't wait for the client

This is the issue #50 fixed for Twisted serving. After the listener sends its close frame and returns, hypercorn terminates the TCP connection right away. When the client's close frame arrives on that closed socket, the kernel resets the connection. If the reset gets to the client before it reads, recv raises ConnectionResetError instead of returning b"". RFC 6455 section 5.5.1 says the server should close the TCP connection only after it has both sent and received a close frame.

  • ASGIWebSocketAdapter.close() now records that the server sent its close frame.
  • When the listener returns, ASGIAdapter.handle_websocket awaits wait_closed(), which waits for the client's websocket.disconnect for up to ASGIWebSocketAdapter.close_timeout (5 seconds, the same as the Twisted channel's closeTimeout). This happens on the event loop, so no listener thread is held. Frames the client sends in the meantime are dropped.
  • The server's graceful shutdown now waits up to close_timeout for clients that never echo the close frame. Four websocket tests left their client open, and each took 5 seconds to tear down, so they now call client.close().

Testing

  • test_close_handshake_server_initiated now sleeps 0.1 s before echoing the close frame and 0.1 s before reading. Without the fix the asgi variant fails 5/5; with it, both variants pass 5/5.
  • test_close_iterable_response passes with the injected sleep and 30/30 in a loop.
  • The full suite passed 10/10 in a loop locally (211 tests, about 43 s per run, the same as before the change).

🤖 Generated with Claude Code

…bsockets

test_close_iterable_response raced against the server closing the iterable
right after sending the last chunk. The iterable now holds the server until
the test has checked that it's still open.

test_close_handshake_server_initiated[asgi] failed when the server terminated
the TCP connection before the client echoed the close frame: the echo then
arrived on a closed socket and the kernel reset the connection. The ASGI
adapter now waits for the client's disconnect after sending its close frame,
up to close_timeout, before the app returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bentsku
bentsku merged commit 18b2768 into main Oct 14, 2026
7 checks passed
@bentsku
bentsku deleted the aws-2208-flaky-tests branch October 14, 2026 16:54
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