From f11e998b2b7bd37d47a252e858ae4112595c690c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:00:36 +0000 Subject: [PATCH 1/3] Initial plan From f206c9e34110821ad23922ce7d14cc268701e58a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:05:50 +0000 Subject: [PATCH 2/3] Add BrokenPipeError handling for client disconnections Co-authored-by: ondratu <6469029+ondratu@users.noreply.github.com> --- poorwsgi/wsgi.py | 5 ++ tests/test_broken_pipe.py | 167 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 tests/test_broken_pipe.py diff --git a/poorwsgi/wsgi.py b/poorwsgi/wsgi.py index 7383bcd..aec8abd 100644 --- a/poorwsgi/wsgi.py +++ b/poorwsgi/wsgi.py @@ -1209,6 +1209,11 @@ def __request__(self, env, start_response): # noqa: C901 except HTTPException as http_err: # HTTP_RANGE_NOT_SATISFIABLE case response = http_err.make_response() return response(start_response) + except (BrokenPipeError, ConnectionResetError, + ConnectionAbortedError) as err: + # Client disconnected before or during response sending + log.info("Client disconnected: %s", str(err)) + return () def __call__(self, env, start_response): """Callable define for Application instance. diff --git a/tests/test_broken_pipe.py b/tests/test_broken_pipe.py new file mode 100644 index 0000000..0108d43 --- /dev/null +++ b/tests/test_broken_pipe.py @@ -0,0 +1,167 @@ +"""Unit test for BrokenPipeError handling.""" +from io import BytesIO +from time import time + +import pytest + +from poorwsgi.response import Response +from poorwsgi.wsgi import Application + + +def test_broken_pipe_on_response_send(): + """Test that BrokenPipeError during response send is handled gracefully.""" + app = Application("test_broken_pipe") + + @app.route('/test') + def test_handler(req): + return "Hello World" + + environ = { + "PATH_INFO": "/test", + "REQUEST_METHOD": "GET", + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "wsgi.url_scheme": "http", + "wsgi.input": BytesIO(b""), + "wsgi.errors": BytesIO(), + "REQUEST_STARTTIME": time(), + } + + def start_response_broken(*_): + """Mock start_response that raises BrokenPipeError.""" + raise BrokenPipeError("Client disconnected") + + # The application should handle BrokenPipeError and return empty iterable + result = app(environ, start_response_broken) + assert result == () + + +def test_connection_reset_on_response_send(): + """Test that ConnectionResetError during response send is handled gracefully.""" + app = Application("test_connection_reset") + + @app.route('/test') + def test_handler(req): + return "Hello World" + + environ = { + "PATH_INFO": "/test", + "REQUEST_METHOD": "GET", + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "wsgi.url_scheme": "http", + "wsgi.input": BytesIO(b""), + "wsgi.errors": BytesIO(), + "REQUEST_STARTTIME": time(), + } + + def start_response_reset(*_): + """Mock start_response that raises ConnectionResetError.""" + raise ConnectionResetError("Connection reset by peer") + + # The application should handle ConnectionResetError and return empty iterable + result = app(environ, start_response_reset) + assert result == () + + +def test_connection_aborted_on_response_send(): + """Test that ConnectionAbortedError during response send is handled gracefully.""" + app = Application("test_connection_aborted") + + @app.route('/test') + def test_handler(req): + return "Hello World" + + environ = { + "PATH_INFO": "/test", + "REQUEST_METHOD": "GET", + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "wsgi.url_scheme": "http", + "wsgi.input": BytesIO(b""), + "wsgi.errors": BytesIO(), + "REQUEST_STARTTIME": time(), + } + + def start_response_aborted(*_): + """Mock start_response that raises ConnectionAbortedError.""" + raise ConnectionAbortedError("Software caused connection abort") + + # The application should handle ConnectionAbortedError and return empty iterable + result = app(environ, start_response_aborted) + assert result == () + + +def test_broken_pipe_during_iteration(): + """Test that BrokenPipeError during response iteration is handled gracefully.""" + app = Application("test_broken_pipe_iteration") + + @app.route('/test') + def test_handler(req): + return "Hello World" + + environ = { + "PATH_INFO": "/test", + "REQUEST_METHOD": "GET", + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "wsgi.url_scheme": "http", + "wsgi.input": BytesIO(b""), + "wsgi.errors": BytesIO(), + "REQUEST_STARTTIME": time(), + } + + class BrokenResponse: + """Mock response object that raises BrokenPipeError during iteration.""" + def __call__(self, start_response): + start_response("200 OK", []) + raise BrokenPipeError("Broken pipe during iteration") + + # Monkey-patch the Response class to raise BrokenPipeError + original_call = Response.__call__ + + def broken_call(self, start_response): + start_response("200 OK", []) + raise BrokenPipeError("Broken pipe during iteration") + + Response.__call__ = broken_call + + try: + # The application should handle BrokenPipeError and return empty iterable + result = app(environ, lambda *args: None) + assert result == () + finally: + # Restore original __call__ method + Response.__call__ = original_call + + +def test_normal_response_still_works(): + """Test that normal responses still work after adding BrokenPipeError handling.""" + app = Application("test_normal_response") + + @app.route('/test') + def test_handler(req): + return "Hello World" + + environ = { + "PATH_INFO": "/test", + "REQUEST_METHOD": "GET", + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "wsgi.url_scheme": "http", + "wsgi.input": BytesIO(b""), + "wsgi.errors": BytesIO(), + "REQUEST_STARTTIME": time(), + } + + response_data = [] + + def start_response(status, headers): + """Normal start_response that collects response data.""" + response_data.append((status, headers)) + + # Normal response should still work + result = app(environ, start_response) + assert result is not None + assert len(response_data) == 1 + assert "200" in response_data[0][0] From 555f576e4fc9466e65549d7689f3daf2d9eb4f4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:07:33 +0000 Subject: [PATCH 3/3] Refactor tests to use pytest fixture and remove dead code Co-authored-by: ondratu <6469029+ondratu@users.noreply.github.com> --- tests/test_broken_pipe.py | 90 ++++++++++----------------------------- 1 file changed, 22 insertions(+), 68 deletions(-) diff --git a/tests/test_broken_pipe.py b/tests/test_broken_pipe.py index 0108d43..705c5c6 100644 --- a/tests/test_broken_pipe.py +++ b/tests/test_broken_pipe.py @@ -8,15 +8,10 @@ from poorwsgi.wsgi import Application -def test_broken_pipe_on_response_send(): - """Test that BrokenPipeError during response send is handled gracefully.""" - app = Application("test_broken_pipe") - - @app.route('/test') - def test_handler(req): - return "Hello World" - - environ = { +@pytest.fixture +def base_environ(): + """Create a base WSGI environ dict for testing.""" + return { "PATH_INFO": "/test", "REQUEST_METHOD": "GET", "SERVER_NAME": "localhost", @@ -27,16 +22,25 @@ def test_handler(req): "REQUEST_STARTTIME": time(), } + +def test_broken_pipe_on_response_send(base_environ): + """Test that BrokenPipeError during response send is handled gracefully.""" + app = Application("test_broken_pipe") + + @app.route('/test') + def test_handler(req): + return "Hello World" + def start_response_broken(*_): """Mock start_response that raises BrokenPipeError.""" raise BrokenPipeError("Client disconnected") # The application should handle BrokenPipeError and return empty iterable - result = app(environ, start_response_broken) + result = app(base_environ, start_response_broken) assert result == () -def test_connection_reset_on_response_send(): +def test_connection_reset_on_response_send(base_environ): """Test that ConnectionResetError during response send is handled gracefully.""" app = Application("test_connection_reset") @@ -44,27 +48,16 @@ def test_connection_reset_on_response_send(): def test_handler(req): return "Hello World" - environ = { - "PATH_INFO": "/test", - "REQUEST_METHOD": "GET", - "SERVER_NAME": "localhost", - "SERVER_PORT": "80", - "wsgi.url_scheme": "http", - "wsgi.input": BytesIO(b""), - "wsgi.errors": BytesIO(), - "REQUEST_STARTTIME": time(), - } - def start_response_reset(*_): """Mock start_response that raises ConnectionResetError.""" raise ConnectionResetError("Connection reset by peer") # The application should handle ConnectionResetError and return empty iterable - result = app(environ, start_response_reset) + result = app(base_environ, start_response_reset) assert result == () -def test_connection_aborted_on_response_send(): +def test_connection_aborted_on_response_send(base_environ): """Test that ConnectionAbortedError during response send is handled gracefully.""" app = Application("test_connection_aborted") @@ -72,27 +65,16 @@ def test_connection_aborted_on_response_send(): def test_handler(req): return "Hello World" - environ = { - "PATH_INFO": "/test", - "REQUEST_METHOD": "GET", - "SERVER_NAME": "localhost", - "SERVER_PORT": "80", - "wsgi.url_scheme": "http", - "wsgi.input": BytesIO(b""), - "wsgi.errors": BytesIO(), - "REQUEST_STARTTIME": time(), - } - def start_response_aborted(*_): """Mock start_response that raises ConnectionAbortedError.""" raise ConnectionAbortedError("Software caused connection abort") # The application should handle ConnectionAbortedError and return empty iterable - result = app(environ, start_response_aborted) + result = app(base_environ, start_response_aborted) assert result == () -def test_broken_pipe_during_iteration(): +def test_broken_pipe_during_iteration(base_environ): """Test that BrokenPipeError during response iteration is handled gracefully.""" app = Application("test_broken_pipe_iteration") @@ -100,23 +82,6 @@ def test_broken_pipe_during_iteration(): def test_handler(req): return "Hello World" - environ = { - "PATH_INFO": "/test", - "REQUEST_METHOD": "GET", - "SERVER_NAME": "localhost", - "SERVER_PORT": "80", - "wsgi.url_scheme": "http", - "wsgi.input": BytesIO(b""), - "wsgi.errors": BytesIO(), - "REQUEST_STARTTIME": time(), - } - - class BrokenResponse: - """Mock response object that raises BrokenPipeError during iteration.""" - def __call__(self, start_response): - start_response("200 OK", []) - raise BrokenPipeError("Broken pipe during iteration") - # Monkey-patch the Response class to raise BrokenPipeError original_call = Response.__call__ @@ -128,14 +93,14 @@ def broken_call(self, start_response): try: # The application should handle BrokenPipeError and return empty iterable - result = app(environ, lambda *args: None) + result = app(base_environ, lambda *args: None) assert result == () finally: # Restore original __call__ method Response.__call__ = original_call -def test_normal_response_still_works(): +def test_normal_response_still_works(base_environ): """Test that normal responses still work after adding BrokenPipeError handling.""" app = Application("test_normal_response") @@ -143,17 +108,6 @@ def test_normal_response_still_works(): def test_handler(req): return "Hello World" - environ = { - "PATH_INFO": "/test", - "REQUEST_METHOD": "GET", - "SERVER_NAME": "localhost", - "SERVER_PORT": "80", - "wsgi.url_scheme": "http", - "wsgi.input": BytesIO(b""), - "wsgi.errors": BytesIO(), - "REQUEST_STARTTIME": time(), - } - response_data = [] def start_response(status, headers): @@ -161,7 +115,7 @@ def start_response(status, headers): response_data.append((status, headers)) # Normal response should still work - result = app(environ, start_response) + result = app(base_environ, start_response) assert result is not None assert len(response_data) == 1 assert "200" in response_data[0][0]