From 049bbf7a7b8e1271b575a74a17a3dde57044b161 Mon Sep 17 00:00:00 2001 From: Gyanu Date: Sat, 29 Aug 2026 10:39:27 +0530 Subject: [PATCH 1/2] Don't append a 500 after the response has already started Worker timeout raises SystemExit, which skipped the headers_sent guard. --- docs/content/news.md | 9 +++++++++ gunicorn/workers/base_async.py | 6 +++--- gunicorn/workers/gthread.py | 6 +++--- gunicorn/workers/sync.py | 6 +++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/content/news.md b/docs/content/news.md index 76c533b2dd..2f83d23a81 100644 --- a/docs/content/news.md +++ b/docs/content/news.md @@ -1,6 +1,15 @@ # Changelog +## Unreleased + +### Bug Fixes + +- Worker timeout used to dump a 500 response onto a body that had already + started, because `sys.exit()` from the abort handler is a `BaseException`. + Once headers are out we just close the connection. + ([#3410](https://github.com/benoitc/gunicorn/issues/3410)). + ## 26.2.0 - 2026-08-24 ### New Features diff --git a/gunicorn/workers/base_async.py b/gunicorn/workers/base_async.py index 9adc913a7e..3686e1f481 100644 --- a/gunicorn/workers/base_async.py +++ b/gunicorn/workers/base_async.py @@ -306,10 +306,10 @@ def handle_request(self, listener_name, req, sock, addr): # If the original exception was a socket.error we delegate # handling it to the caller (where handle() might ignore it) util.reraise(*sys.exc_info()) - except Exception: + except BaseException: if resp and resp.headers_sent: - # If the requests have already been sent, we should close the - # connection to indicate the error. + # Timeout abort is SystemExit. If we already started the + # response, writing a 500 on the same socket corrupts it. self.log.exception("Error handling request") try: sock.shutdown(socket.SHUT_RDWR) diff --git a/gunicorn/workers/gthread.py b/gunicorn/workers/gthread.py index 08391eb1d0..c071139120 100644 --- a/gunicorn/workers/gthread.py +++ b/gunicorn/workers/gthread.py @@ -730,10 +730,10 @@ def handle_request(self, req, conn): except OSError: # pass to next try-except level util.reraise(*sys.exc_info()) - except Exception: + except BaseException: if resp and resp.headers_sent: - # If the requests have already been sent, we should close the - # connection to indicate the error. + # Timeout abort is SystemExit. If we already started the + # response, writing a 500 on the same socket corrupts it. self.log.exception("Error handling request") util.close_graceful(conn.sock) raise StopIteration() diff --git a/gunicorn/workers/sync.py b/gunicorn/workers/sync.py index c11597f22e..f0c5715c11 100644 --- a/gunicorn/workers/sync.py +++ b/gunicorn/workers/sync.py @@ -197,10 +197,10 @@ def handle_request(self, listener, req, client, addr): except OSError: # pass to next try-except level util.reraise(*sys.exc_info()) - except Exception: + except BaseException: if resp and resp.headers_sent: - # If the requests have already been sent, we should close the - # connection to indicate the error. + # Timeout abort is SystemExit. If we already started the + # response, writing a 500 on the same socket corrupts it. self.log.exception("Error handling request") util.close_graceful(client) raise StopIteration() From eb283b2d69e728817c7e75aa0aee0361845618c6 Mon Sep 17 00:00:00 2001 From: Gyanu Date: Mon, 31 Aug 2026 09:37:30 +0530 Subject: [PATCH 2/2] Catch Exception and SystemExit instead of BaseException. --- gunicorn/workers/base_async.py | 2 +- gunicorn/workers/gthread.py | 2 +- gunicorn/workers/sync.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gunicorn/workers/base_async.py b/gunicorn/workers/base_async.py index 3686e1f481..a3aea4c5f0 100644 --- a/gunicorn/workers/base_async.py +++ b/gunicorn/workers/base_async.py @@ -306,7 +306,7 @@ def handle_request(self, listener_name, req, sock, addr): # If the original exception was a socket.error we delegate # handling it to the caller (where handle() might ignore it) util.reraise(*sys.exc_info()) - except BaseException: + except (Exception, SystemExit): if resp and resp.headers_sent: # Timeout abort is SystemExit. If we already started the # response, writing a 500 on the same socket corrupts it. diff --git a/gunicorn/workers/gthread.py b/gunicorn/workers/gthread.py index c071139120..a5a61a460c 100644 --- a/gunicorn/workers/gthread.py +++ b/gunicorn/workers/gthread.py @@ -730,7 +730,7 @@ def handle_request(self, req, conn): except OSError: # pass to next try-except level util.reraise(*sys.exc_info()) - except BaseException: + except (Exception, SystemExit): if resp and resp.headers_sent: # Timeout abort is SystemExit. If we already started the # response, writing a 500 on the same socket corrupts it. diff --git a/gunicorn/workers/sync.py b/gunicorn/workers/sync.py index f0c5715c11..cde3fb2128 100644 --- a/gunicorn/workers/sync.py +++ b/gunicorn/workers/sync.py @@ -197,7 +197,7 @@ def handle_request(self, listener, req, client, addr): except OSError: # pass to next try-except level util.reraise(*sys.exc_info()) - except BaseException: + except (Exception, SystemExit): if resp and resp.headers_sent: # Timeout abort is SystemExit. If we already started the # response, writing a 500 on the same socket corrupts it.