Skip to content

Gunicorn injects "HTTP/1.1 500 Internal Server Error" into body when worker is terminated while streaming response #3410

@psrok1

Description

@psrok1

Hi,

one of users tried to download a file via service based on Flask, gunicorn and sync workers. He was using a very slow connection and file was pretty big, so connection was interrupted after 60 seconds due to worker timeout. Endpoint was supporting Range header, so wget made a new request for a missing part of the file and download was finished.

Unfortunately, the file appeared to be incorrect. After inspection, we noticed that ISE 500 headers and body were mixed into content:

Image

This situation can be easily reproduced using that code:

import time
from flask import Flask, Response

app = Flask(__name__)

def stream_endlessly():
    while True:
        yield b'a'*16 + b'\n'
        time.sleep(1)

@app.route('/')
def index():
    return Response(stream_endlessly(), mimetype='text/plain')

If we spawn it using gunicorn app:app -t 10 and make a request curl http://127.0.0.1:8000 --http1.0 -vvv (using HTTP 1.0 to avoid chunked encoding), we will see the following output:

$ curl http://127.0.0.1:8000 --http1.0 -vvv
*   Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000
> GET / HTTP/1.0
> Host: 127.0.0.1:8000
> User-Agent: curl/8.5.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: gunicorn
< Date: Tue, 12 Aug 2025 18:51:19 GMT
< Connection: close
< Content-Type: text/plain; charset=utf-8
< 
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
HTTP/1.1 500 Internal Server Error
Connection: close
Content-Type: text/html
Content-Length: 141

<html>
  <head>
    <title>Internal Server Error</title>
  </head>
  <body>
    <h1><p>Internal Server Error</p></h1>
    
  </body>
</html>
* Closing connection

The root cause is well described by the stack trace:

[2025-08-12 20:51:35 +0200] [46057] [CRITICAL] WORKER TIMEOUT (pid:46085)
[2025-08-12 20:51:35 +0200] [46085] [ERROR] Error handling request /
Traceback (most recent call last):
  File "/home/psrok1/gunicorn-bug-20250812/.venv/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 134, in handle
    self.handle_request(listener, req, client, addr)
  File "/home/psrok1/gunicorn-bug-20250812/.venv/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 182, in handle_request
    for item in respiter:
  File "/home/psrok1/gunicorn-bug-20250812/.venv/lib/python3.12/site-packages/werkzeug/wsgi.py", line 256, in __next__
    return self._next()
           ^^^^^^^^^^^^
  File "/home/psrok1/gunicorn-bug-20250812/.venv/lib/python3.12/site-packages/werkzeug/wrappers/response.py", line 32, in _iter_encoded
    for item in iterable:
  File "/home/psrok1/gunicorn-bug-20250812/app.py", line 9, in stream_endlessly
    time.sleep(1)
  File "/home/psrok1/gunicorn-bug-20250812/.venv/lib/python3.12/site-packages/gunicorn/workers/base.py", line 204, in handle_abort
    sys.exit(1)
SystemExit: 1
[2025-08-12 20:51:35 +0200] [46085] [INFO] Worker exiting (pid: 46085)

When regular Exceptions are handled, handle_request checks if headers were already sent (

if resp and resp.headers_sent:
). If yes, it means that response is partially sent and best we can do is to close the connection. Unfortunately, BaseExceptions (including SystemExit) are not handled, so we land in this part:
self.handle_error(req, client, addr, e)
, which finally writes additional headers and body into the socket (
def write_error(sock, status_int, reason, mesg):
).

I think that Exception handler in handle_request should be extended to handle BaseException as well.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions