Files
gost/tests/e2e/scripts/http_big_body.py
ginuerzh 94ccb5c706 tests(e2e): a quic tunnel must survive aborted downloads
Data the peer already sent stays unread in the stream and keeps
occupying the connection level receive window of the quic session. When
the local app aborts a download, that data is never consumed, so the
credit is only returned when the read side of the stream is cancelled.

Without the cancellation the window drains. The initial stream window is
512 KB and the initial connection window is 768 KB, so a few aborted
downloads are enough to exhaust it. Once that happens the peer cannot
send data on any stream of the session: new requests still reach the
server, the server still acknowledges, there is no idle timeout and no
error in the log, and the client waits forever for a response. Only a
new session clears it.

The test aborts three downloads of a body that is much larger than the
flow control window, and then checks that the tunnel still forwards. The
client reads very slowly, so the data piles up in the connection that
carries it.

Verified against go-gost/x#135: with the fix reverted the test fails
with a timeout on the request that follows the aborted downloads, which
is the reported symptom.
2026-10-21 10:42:56 +00:00

45 lines
1.5 KiB
Python

"""HTTP server that streams a large body.
The body is much larger than the QUIC flow control window, so a client
that stops reading fills the window of the connection that carries it.
That is what a browser does when the user cancels a download, and it is
what the flow control credit leak needs to reproduce.
Usage: http_big_body.py [port] [total_bytes] [chunk_bytes] [delay_seconds]
"""
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 5679
TOTAL = int(sys.argv[2]) if len(sys.argv) > 2 else 4 * 1024 * 1024
CHUNK = int(sys.argv[3]) if len(sys.argv) > 3 else 64 * 1024
DELAY = float(sys.argv[4]) if len(sys.argv) > 4 else 0.01
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Length", str(TOTAL))
self.send_header("Content-Type", "application/octet-stream")
self.end_headers()
chunk = b"x" * CHUNK
sent = 0
try:
while sent < TOTAL:
n = min(CHUNK, TOTAL - sent)
self.wfile.write(chunk[:n])
self.wfile.flush()
sent += n
if DELAY > 0:
time.sleep(DELAY)
except (BrokenPipeError, ConnectionResetError, OSError):
# The client aborted the download.
pass
def log_message(self, format, *args):
return
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()