Skip to content

Stop handing back pipelined requests the parser buffered - #13356

Merged
Dreamsorcerer merged 5 commits into
aio-libs:masterfrom
rodrigobnogueira:fix-parser-tail-duplication
Aug 9, 2026
Merged

Stop handing back pipelined requests the parser buffered#13356
Dreamsorcerer merged 5 commits into
aio-libs:masterfrom
rodrigobnogueira:fix-parser-tail-duplication

Conversation

@rodrigobnogueira

@rodrigobnogueira rodrigobnogueira commented Aug 9, 2026

Copy link
Copy Markdown
Member

What do these changes do?

When the pipelined message queue fills, the pure-Python parser buffers the unparsed remainder in self._tail and returns it (http_parser.py, queue-full break). The caller therefore holds a second copy of bytes the parser is already keeping, and the next feed_data() prepends the retained copy onto the returned one.

data_received() discards the returned tail unless the request upgraded, so ordinary pipelining never notices. finish_response() stores it unconditionally, and that is the path taken when a handler answers an Upgrade: request with a normal response. Pipelining more requests than the queue holds behind such a request then makes the server either serve them repeatedly or grow the buffer by a copy of itself per handled request.

Measured on this branch's parent, pure-Python parser, one upgrade request followed by 40 pipelined GETs in a single write:

before after
responses connection never settles; handlers re-run indefinitely 41
distinct paths served 41
repeat dispatches unbounded 0

The fix clears the returned buffer, which is exactly what the incomplete-request-line branch a few lines below already does. The C parser never returned the tail, so the duplication is pure-Python only, and the added parser test passes on the C backend before and after.

The second defect is not parser-specific. finish_response() appends to the message queue without the pause bookkeeping data_received() performs, so _msg_queue_paused stays False, start() never calls _resume_msg_queue_reading(), and nothing ever feeds the parser the remainder it kept. The requests past the queue limit simply go unanswered (33 of 41 above). This half affects the C parser too — arguably more cleanly, since before the fix the pure-Python duplicate kept _message_tail non-empty and so kept pumping. Reverting both source files and running the new functional test under the C extensions fails on its 10s timeout. Pausing in finish_response() lets the existing drain resume them.

Are there changes in behavior for the user?

Requests pipelined behind a declined upgrade past the queue limit are answered instead of being left buffered — that applies to both parsers. With the pure-Python parser they are additionally handled exactly once rather than repeatedly. No API change.

Is it a substantial burden for the maintainers to support this?

No. Five lines of parser change plus the queue-pause call that the neighbouring code path already makes, and both new tests fail without them.

Related issue number

None.

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes — N/A, no user-facing API change
  • If you provide code modification, please add yourself to CONTRIBUTORS.txt — already listed
  • Add a new news fragment into the CHANGES/ folder
Test run

Both parser backends, C extensions built (make cythonize + editable install):

$ pytest tests/test_http_parser.py tests/test_web_protocol.py \
         tests/test_web_functional.py tests/test_web_server.py
1015 passed, 6 deselected, 3 xfailed          # C extensions
599 passed, 35 skipped, 4 deselected          # AIOHTTP_NO_EXTENSIONS=1

Revert check, with the source changes reverted and the tests kept:

# pure-Python parser
FAILED tests/test_http_parser.py::test_max_msg_queue_size_keeps_tail_to_itself[py-parser]
# C extensions - the stalled-request half
FAILED tests/test_web_functional.py::test_http1_pipelined_behind_declined_upgrade_served_once
       (1 failed in 10.06s - the wait_for timeout, requests stranded)

Under AIOHTTP_NO_EXTENSIONS=1 that functional test does not fail cleanly when reverted: the repeated dispatch keeps the loop busy and the test's own timeout never fires. With the C extensions there is no repeated dispatch, so it fails on the timeout as shown above. It passes in well under a second either way with the change applied.

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.
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label Aug 9, 2026
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.99%. Comparing base (288cf46) to head (a8e71e2).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #13356   +/-   ##
=======================================
  Coverage   98.99%   98.99%           
=======================================
  Files         132      132           
  Lines       49156    49189   +33     
  Branches     2560     2562    +2     
=======================================
+ Hits        48661    48694   +33     
  Misses        371      371           
  Partials      124      124           
Flag Coverage Δ
Autobahn 22.09% <6.06%> (-0.02%) ⬇️
CI-GHA 98.90% <100.00%> (+<0.01%) ⬆️
OS-Linux 98.67% <100.00%> (+<0.01%) ⬆️
OS-Windows 97.03% <100.00%> (+<0.01%) ⬆️
OS-macOS 97.93% <100.00%> (+<0.01%) ⬆️
Py-3.10 98.12% <100.00%> (+<0.01%) ⬆️
Py-3.11 98.38% <100.00%> (+<0.01%) ⬆️
Py-3.12 98.47% <100.00%> (+<0.01%) ⬆️
Py-3.13 98.45% <100.00%> (+<0.01%) ⬆️
Py-3.14 98.47% <100.00%> (+<0.01%) ⬆️
Py-3.14t 97.55% <100.00%> (-0.01%) ⬇️
Py-pypy-3.11 97.40% <100.00%> (-0.02%) ⬇️
VM-macos 97.93% <100.00%> (+<0.01%) ⬆️
VM-ubuntu 98.67% <100.00%> (+<0.01%) ⬆️
VM-windows 97.03% <100.00%> (+<0.01%) ⬆️
cython-coverage 38.11% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@codspeed-hq

codspeed-hq Bot commented Aug 9, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 10.18%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 83 untouched benchmarks
⏩ 83 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_resolve_gitapi 660 ms 599 ms +10.18%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing rodrigobnogueira:fix-parser-tail-duplication (a8e71e2) with master (288cf46)

Open in CodSpeed

Footnotes

  1. 83 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@rodrigobnogueira rodrigobnogueira added backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot backport-3.15 Trigger automatic backporting to the 3.15 release branch by Patchback robot labels Aug 9, 2026
Comment thread aiohttp/web_protocol.py Outdated
Comment thread aiohttp/http_parser.py Outdated
Co-authored-by: Sam Bull <aa6bs0@sambull.org>
@Dreamsorcerer
Dreamsorcerer merged commit 72eaa42 into aio-libs:master Aug 9, 2026
50 checks passed
@patchback

patchback Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Backport to 3.14: 💚 backport PR created

✅ Backport PR branch: patchback/backports/3.14/72eaa429cf89b1e20212590f3e704444239569fb/pr-13356

Backported as #13360

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

@patchback

patchback Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Backport to 3.15: 💚 backport PR created

✅ Backport PR branch: patchback/backports/3.15/72eaa429cf89b1e20212590f3e704444239569fb/pr-13356

Backported as #13361

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

Dreamsorcerer pushed a commit that referenced this pull request Aug 9, 2026
…sts the parser buffered (#13361)

**This is a backport of PR #13356 as merged into master
(72eaa42).**

---------

Co-authored-by: Rodrigo Nogueira <rodrigo.b.nogueira@gmail.com>
Dreamsorcerer pushed a commit that referenced this pull request Aug 9, 2026
…sts the parser buffered (#13360)

**This is a backport of PR #13356 as merged into master
(72eaa42).**

---------

Co-authored-by: Rodrigo Nogueira <rodrigo.b.nogueira@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot backport-3.15 Trigger automatic backporting to the 3.15 release branch by Patchback robot bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants