Repository navigation
fix: combine repeated request headers instead of keeping the last - #7588
Conversation
_decode_asgi_headers built a dict from the ASGI header pairs, so a repeated field kept only its last value: router.headers.cookie lost every cookie but the last, and client_ip came from the last x-forwarded-for field instead of the first hop. Join repeated cookie fields with "; " and every other repeated field with ", ", in order. Refs reflex-dev#7521
[Medium impact] The PR appears safe to merge. SummaryRepeated request headers now keep their values in order.
Reviews (2) · Last reviewed commit: "perf: decode ASGI headers with the defau..." · Reviewed by Greptile |
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
Merging this PR will degrade performance by 4.47%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_route_argument_extraction |
115.2 µs | 120.5 µs | -4.47% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing ybai08:fix/7521-repeated-headers (0dfb96c) with main (75a08e1)
Port upstream's repeated-header combining (reflex-dev#7588) from the removed reflex/app.py helper to _decode_asgi_headers in reflex/event_namespace.py, which both transports share, keeping the branch's Latin-1 fallback. Its tests move from tests/units/test_app.py to tests/units/test_event_namespace.py.
Refs #7521. This is part 1 of the split proposed on the issue: steps 1 and 2 of the fix. It leaves
raw_headersmulti-value access (getlist, step 3) to a follow-up PR._decode_asgi_headersbuilt a dict from the ASGI header pairs, so a repeated field kept only its last value. ASGI keeps repeats as separate pairs. The function now combines them in order:cookiefields are joined with"; "(RFC 9113 §8.2.3);", "(RFC 9110 §5.3).It still returns the same
dict[str, str], soRouteVar.HEADERSand everything that reads it keep their shape. With the combined values:router.headers.cookiehas every cookie;router.session.client_ipcomes from the firstx-forwarded-forhop, because the existing.partition(",")[0]now seesfirst, second;raw_headers[name]shows the combined value.The credential filtering from #7360 works on the joined dict, so a repeated
cookieis still dropped entirely from what reaches the frontend.Tests
test_decode_asgi_headers: the old "later duplicates win" assertion now expectsa=1; b=2. A new case interleaves twox-forwarded-forfields with another header and expects10.0.0.1, 10.0.0.2, 10.0.0.3.test_static_router_data_keeps_every_repeated_header_field(new) goes throughEventNamespace._build_static_router_datawith twocookieand twox-forwarded-forfields. It checks the joined headers, thatCLIENT_IPis the first hop (203.0.113.7, not the proxy), and thatHeaderData.from_router_data(...).cookiekeeps both cookies.Both fail on
mainwith the issue's symptom ({'cookie': 'b=2'},'other=2'). With the change, all oftests/units/test_app.pypasses (194).I also ran
uv run pytest tests/units --cov --no-cov-on-fail --cov-report=: 12014 passed, 26 skipped, total coverage 80.03%.ruff check,ruff format --checkandpyrighton the two changed files are clean (0 errors).Performance
The function now runs a loop instead of a comprehension. I measured that with
timeiton the benchmark's unique-header shape on macOS arm64. It's 1.10–1.14× slower: 0.27 vs 0.25 µs for 4 headers, 4.08 vs 3.57 µs for 64. It is only called from_build_static_router_data, once per connection, not per event.Not in this PR
Step 3: keeping every field in
raw_headersforgetlist()andmulti_items(). The open questions for it are on the issue.