Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bug] gunicorn re-chunks responses which were already chunked #3322

Open
twitchyliquid64 opened this issue Nov 4, 2024 · 0 comments
Open

Comments

@twitchyliquid64
Copy link

twitchyliquid64 commented Nov 4, 2024

Heya! I think I found a bug in gunicorn's handling of responses which were already chunked.

The issue

If a handler upstream to gunicorn:

  1. Does not set Content-Length
  2. Sets Transfer-Encoding: chunked
  3. Streams back data which is already chunked (i.e. response data is already framed as <length>\r\n<data>\r\n)

gunicorn wraps the already-chunked content in chunks, corrupting the data stream with erroneous framing.

Suspected code

is_chunked is used to turn on gunicorn's own chunking, but does not consider if the response is already chunked.

def is_chunked(self):
# Only use chunked responses when the client is
# speaking HTTP/1.1 or newer and there was
# no Content-Length header set.
if self.response_length is not None:
return False
elif self.req.version <= (1, 0):
return False
elif self.req.method == 'HEAD':
# Responses to a HEAD request MUST NOT contain a response body.
return False
elif self.status_code in (204, 304):
# Do not use chunked responses when the response is guaranteed to
# not have a response body.
return False
return True

Repro steps

A flask app which streams its own response like this. Works fine with other WSGI wrappers:

@some_blueprint.route('/stream', methods=['POST'])
def stream():
  r = Response(chunk_generator(), content_type='application/chunked-json')
  r.automatically_set_content_length = False
  r.direct_passthrough = True
  r.headers['Transfer-Encoding'] = 'chunked'
  return r

def chunk_generator() -> Generator[bytes, None, None]:
  for x in range(50):
    c = wire_chunk('some bytes'.encode('utf-8'))
    yield c
  yield '0\r\n\r\n'.encode('utf-8') # Symbolizes end of stream

def wire_chunk(data: bytes) -> bytes:
    data_len = len(data)
    return f'{data_len:x}'.encode('utf-8') + '\r\n'.encode('utf-8') + data + '\r\n'.encode('utf-8')
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

No branches or pull requests

1 participant