Early HTTP/2 response during an upload is lost until the read timeout
Maintainers usually reply within 1 day
@Kludex is already working on this.
Since Oct 23, 2026.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Stale
- Tech stack
- python
- Domain
- backend, networking
Research direction
Start with _send_request_body and _receive_response in the HTTP/2 connection implementation, then trace how flow-control waits queue stream events. Reproduce the issue with the supplied script, which expects status 413 but times out. Done means the client stops uploading on StreamEnded or StreamReset, handles the reset correctly, and returns the queued response; linked pull requests 1295 and 1298 indicate work is already underway.
Written by the indexing model from the issue text.
Description
When a server answers an HTTP/2 upload before the body is complete, the client keeps uploading. If the window is exhausted it waits for a WINDOW_UPDATE that never comes and fails with ReadTimeout; if the server reset the stream, the next DATA frame raises LocalProtocolError. The response is lost either way.
RFC 9113 §8.1 covers this case: a server "MAY request that the client abort transmission of a request without error by sending a RST_STREAM with an error code of NO_ERROR after sending a complete response", and "Clients MUST NOT discard responses as a result of receiving such a RST_STREAM". RFC 9112 §9.5 asks the same of HTTP/1.1 clients, and the HTTP/1.1 implementation already handles it.
Typical trigger: a 413 Content Too Large, 401, or 403 sent as soon as the request headers arrive, while the client is still streaming a large body.
Reproduction
import asyncio
import h2.errors
import h2.settings
import hpack
import hyperframe.frame
import httpcore2
async def body():
for _ in range(10):
yield b"x" * 20_000
class QuietAfterScript(httpcore2.AsyncMockStream):
async def read(self, max_bytes, timeout=None):
if not self._buffer:
await asyncio.sleep(10) # a real server has nothing more to say
return await super().read(max_bytes, timeout)
async def main():
stream = QuietAfterScript(
[
hyperframe.frame.SettingsFrame(settings={h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 1000}).serialize(),
hyperframe.frame.HeadersFrame(
stream_id=1,
data=hpack.Encoder().encode([(b":status", b"413")]),
flags=["END_HEADERS", "END_STREAM"],
).serialize(),
hyperframe.frame.RstStreamFrame(stream_id=1, error_code=h2.errors.ErrorCodes.NO_ERROR).serialize(),
]
)
origin = httpcore2.Origin(b"https", b"example.com", 443)
async with httpcore2.AsyncHTTP2Connection(origin=origin, stream=stream) as conn:
response = await asyncio.wait_for(
conn.request("POST", "https://example.com/", headers={"content-length": "200000"}, content=body()),
timeout=2,
)
print(response.status)
asyncio.run(main())
Expected: 413. Actual: TimeoutError (ReadTimeout under httpx2 defaults) after 4 of 10 chunks.
Cause
_send_request_body iterates the whole body before _receive_response is called. The server's frames are read and queued into the stream's event list by the flow-control wait, but the send loop only ever looks at the flow-control window.
Fix
Check the stream's queued events in the send loop. On StreamEnded or StreamReset, stop uploading, send RST_STREAM(NO_ERROR) unless the server already reset the stream (what Go's net/http does), and return the queued response.
- Dominant language
- Python
- Stars
- 1.6k
- Forks
- 103
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 25
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pydantic/httpx2
-
Please revert aiter_bytes signature to AsyncIteratorMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
pydantic/httpx2#1158 · 2 comments ·
Maintainers usually reply within 1 day
-
Export AutoBackendMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 1/5 Under an hour Newbie friendliness 92/100
pydantic/httpx2#1131 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
Mention header name error message for invalid header encodingsMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
pydantic/httpx2#864 · 3 comments ·
Maintainers usually reply within 1 day
-
Support DigestAuth with "x-www-authenticate" in first response headerPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
OCA/account-invoicing#2529 ·
Maintainers usually reply within 1 day
-
area/ci
Difficulty 1/5 Under an hour Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
ImageGen generate-batch aborts valid JSONL jobs containing a Unicode line separator in poster textOpenbug imagen skills
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Email validation accepts a trailing newlinePossibly taken @marcello-pichini claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day