Repository navigation
Handle the replayed tail the way data_received does - #13358
rodrigobnogueira wants to merge 8 commits into
Conversation
When the message queue fills, the pure-Python parser stores the unparsed remainder in self._tail and also returns it. The caller gets a second copy of bytes the parser is already holding, and the next feed_data() prepends the retained copy to the returned one. data_received() drops the returned tail unless the request upgraded, so ordinary pipelining is unaffected. finish_response() keeps it unconditionally, though, which is the path taken when a handler answers an upgrade request normally. Pipelining more requests than the queue holds behind such a request then serves them repeatedly, or grows the buffer by a copy of itself per request until memory runs out. The C parser never returned the tail, so only the pure-Python one is affected. Clear the returned buffer, matching what the incomplete-request-line branch already does. finish_response() also appended straight to the queue without the pause bookkeeping data_received() does, so nothing later fed the parser the remainder it kept: the requests past the queue limit went unanswered. Pause there too and let start() resume as it drains.
for more information, see https://pre-commit.ci
finish_response() replays the bytes buffered behind an upgrade request the handler declined. It feeds them to the parser without the two things data_received() does around the same call. A parse error escapes. It is raised before the declined upgrade's own response is written, so that response is lost, the connection is force closed, and a client error is logged as an unhandled exception. The same malformed request on the ordinary path is answered with 400. Queue the error message instead, exactly as data_received does. The upgraded flag from the replay is discarded. When the replayed bytes contain a second upgrade the parser switches to upgraded while this stays False, so the next data_received() takes the parser branch and assigns _message_tail rather than appending to it. The requests already buffered there are dropped, silently with the pure-Python parser and together with the following request under the C one. Keep the flag.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #13358 +/- ##
=======================================
Coverage 98.99% 98.99%
=======================================
Files 132 132
Lines 49156 49246 +90
Branches 2560 2563 +3
=======================================
+ Hits 48661 48753 +92
+ Misses 371 370 -1
+ Partials 124 123 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Merging this PR will not alter performance
Comparing Footnotes
|
The 400 closes the connection, so a single read to EOF collects both responses. The loop needed an end-of-stream break that never ran while the test passed, and tests are measured for coverage.
|
@rodrigobnogueira If this is still relevant, please fix conflicts. |

What do these changes do?
finish_response()replays the bytes buffered behind an upgrade request the handler declined (#12796). It feeds them to the parser without the two thingsdata_received()does around the same call, and both are reachable from the network.A parse error escapes.
data_received()wrapsfeed_data()and turnsHttpProcessingErrorinto a queued 400.finish_response()does not, and it replays beforeresp.prepare()runs — so a malformed request pipelined behind the declined upgrade discards the response already built for the upgrade, force-closes the connection, and logs a routine client error as an unhandled server exception:The identical malformed request on the ordinary path already gets a clean 400.
The upgraded flag is discarded. When the replayed bytes contain a second upgrade, the parser switches to upgraded while
self._upgradedstaysFalse. The nextdata_received()therefore takes the parser branch and assigns_message_tailinstead of appending to it, dropping whatever was buffered there:/pipelineddropped, silently/pipelineddropped and the next request corruptedThe C parser's corruption is worth spelling out, because it is the sharper symptom.
nbis declared at_http_parser.pyx:631and assigned only inside theHPE_PAUSED_UPGRADEbranch at:675, but it is read at:711:Entering
feed_data()already upgraded, without llhttp returningHPE_PAUSED_UPGRADEon that call, therefore slicesdatawith an uninitialisedsize_t. In the reproducer it lands on 1, and the next request line arrives asb"ET /later HTTP/1.1"— the leadingGeaten. Keeping the flag stops the protocol from ever handing the parser that state, so this is a hardening of an invariant the C parser already assumes rather than only a bookkeeping fix.Are there changes in behavior for the user?
A malformed request pipelined behind a declined upgrade is answered with 400 instead of killing the connection and losing the preceding response. Requests buffered behind a second declined upgrade are dispatched instead of dropped. No API change.
Is it a substantial burden for the maintainers to support this?
No. It makes the replay path mirror
data_received(), which is where the same three concerns are already handled.Related issue number
None. Both defects predate #13356 — verified by running each reproducer against
288cf469band against this branch's parent and getting identical results — and both are on the path added in #12796.Checklist
CONTRIBUTORS.txt— already listedCHANGES/folderTest run
Both parser backends, C extensions built:
Both new tests fail with only
web_protocol.pyreverted, on both backends:mypy aiohttp/web_protocol.pyreports the same count as the parent commit, with no errors in the file itself.