Skip to content

Commit cbd27e8

Browse files
authored
Merge pull request #3559 from benleembruggen/fix/http2-asgi-body-duplication
fix: prevent HTTP/2 ASGI body duplication in receive()
2 parents 997eec4 + 8fba44c commit cbd27e8

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

gunicorn/asgi/protocol.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,9 @@ async def receive():
13281328
"more_body": False,
13291329
}
13301330

1331+
if stream._body_complete:
1332+
body_received = True
1333+
13311334
return {
13321335
"type": "http.request",
13331336
"body": chunk,

tests/docker/asgi_compliance/test_http2_asgi.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,35 @@ def test_direct_https_streaming(self, http_client, gunicorn_ssl_url):
406406
response = http_client.get(f"{gunicorn_ssl_url}/stream/streaming?chunks=3")
407407
assert response.status_code == 200
408408
assert "Chunk" in response.text
409+
410+
def test_direct_https_post_echo(self, http_client, gunicorn_ssl_url):
411+
"""Test POST echo directly to gunicorn over HTTPS."""
412+
body = b"HTTP/2 direct echo test"
413+
response = http_client.post(
414+
f"{gunicorn_ssl_url}/http/echo",
415+
content=body
416+
)
417+
assert response.status_code == 200
418+
assert response.content == body
419+
420+
def test_direct_https_post_json(self, http_client, gunicorn_ssl_url):
421+
"""Test POST JSON directly to gunicorn over HTTPS."""
422+
data = {"message": "http2 direct post", "number": 42}
423+
response = http_client.post(
424+
f"{gunicorn_ssl_url}/http/post-json",
425+
json=data
426+
)
427+
assert response.status_code == 200
428+
result = response.json()
429+
assert result["received"]["message"] == "http2 direct post"
430+
assert result["received"]["number"] == 42
431+
432+
def test_direct_https_post_large_body(self, http_client, gunicorn_ssl_url):
433+
"""Test large POST body directly to gunicorn over HTTPS."""
434+
body = b"x" * 100000 # 100KB, spans multiple HTTP/2 DATA frames
435+
response = http_client.post(
436+
f"{gunicorn_ssl_url}/http/echo",
437+
content=body
438+
)
439+
assert response.status_code == 200
440+
assert len(response.content) == 100000

tests/test_http2_stream.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,41 @@ def test_get_body_after_data(self):
477477
assert stream.get_request_body() == b"Test body content"
478478

479479

480+
class TestReadBodyChunk:
481+
"""Test read_body_chunk method."""
482+
483+
@pytest.mark.asyncio
484+
async def test_read_body_chunk_returns_data(self):
485+
conn = MockConnection()
486+
stream = HTTP2Stream(stream_id=1, connection=conn)
487+
stream.state = StreamState.OPEN
488+
489+
stream.receive_data(b"chunk1", end_stream=True)
490+
491+
chunk = await stream.read_body_chunk()
492+
assert chunk == b"chunk1"
493+
494+
@pytest.mark.asyncio
495+
async def test_read_body_chunk_multi_frame(self):
496+
"""Multiple DATA frames should each be returned as separate chunks."""
497+
conn = MockConnection()
498+
stream = HTTP2Stream(stream_id=1, connection=conn)
499+
stream.state = StreamState.OPEN
500+
501+
stream.receive_data(b"part1")
502+
stream.receive_data(b"part2")
503+
stream.receive_data(b"part3", end_stream=True)
504+
505+
chunks = []
506+
for _ in range(3):
507+
chunk = await stream.read_body_chunk()
508+
if chunk is None:
509+
break
510+
chunks.append(chunk)
511+
512+
assert b"".join(chunks) == b"part1part2part3"
513+
514+
480515
class TestGetPseudoHeaders:
481516
"""Test get_pseudo_headers method."""
482517

0 commit comments

Comments
 (0)