Skip to content

fix(probe): parse Transfer-Encoding instead of matching the whole line - #911

Open
atoz96 wants to merge 2 commits into
GeniePod:mainfrom
atoz96:fix/chunked-transfer-encoding-parse
Open

fix(probe): parse Transfer-Encoding instead of matching the whole line#911
atoz96 wants to merge 2 commits into
GeniePod:mainfrom
atoz96:fix/chunked-transfer-encoding-parse

Conversation

@atoz96

@atoz96 atoz96 commented Jul 30, 2026

Copy link
Copy Markdown

Summary

read_http_body detected chunk framing by comparing the whole header line against "transfer-encoding: chunked", so only the single canonical spelling matched. Because a chunked response carries no Content-Length, a miss fell through to the read-to-EOF path and returned the wire bytes verbatim — chunk-size lines inlined into the body, with a 200 status and no error. Fixes #910.

Changes

  • is_chunked parses the header the way parse_content_length two functions below already did: split on the colon, compare the name case-insensitively, and require the final comma-separated coding to be chunked (RFC 7230 §3.3.1 — chunked must be last).
  • Regression test over the three spellings that previously slipped past: Transfer-Encoding:chunked, Transfer-Encoding: chunked, transfer-encoding: gzip, chunked.

Real Behavior Proof

  • I have built and run the affected code locally (or noted why I could not).
  • I have verified the change end-to-end on Jetson hardware.
  • I have NOT verified on Jetson hardware, and I explain the equivalent verification path or validation gap below.

Tested profile / hardware (check all that apply):

  • jetson
  • raspberry_pi
  • portable_sbc
  • laptop
  • mac
  • CI-only / docs-only
  • Not run locally

What I ran

x86_64 Linux laptop (kernel 6.8), no Jetson and no cross-compile toolchain here — so this is CONTRIBUTING route 3 plus route 4's disclosure, not route 1.

This is a pure HTTP-framing path with no hardware, audio, or Home Assistant dependency: the affected code reads bytes off an AsyncRead, and the tests drive it through a real TcpListener on loopback. That makes loopback an equivalent exercise of the changed path rather than a proxy for it. What I cannot verify from here is any Jetson-specific behaviour of the callers — none is expected, since the change is confined to header parsing.

cargo test -p genie-common
cargo test --workspace
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings

Named tests covering the changed path:

  • probe::tests::chunked_body_decoded_for_noncanonical_transfer_encoding (new — the regression)
  • probe::tests::read_chunked_body_decodes_valid_chunks (existing — canonical framing still decodes)
  • probe::tests::read_chunked_body_rejects_oversize_chunk_header (existing — the size guard still fires)
  • probe::tests::parse_content_length callers via the fixed-body tests around it (existing — the non-chunked branch is unchanged)

What I observed

The new test fails on main with the framing inlined into the payload:

assertion `left == right` failed: header was "Transfer-Encoding:chunked"
  left: "5\r\nhello\r\n6\r\n world\r\n0"
 right: "hello world"

After the fix, all three spellings decode to hello world.

Full run on this branch:

genie-common   test result: ok. 138 passed; 0 failed
genie-core     test result: ok. 992 passed; 0 failed; 6 ignored
workspace      no failures across any crate
cargo fmt --all -- --check    clean
cargo clippy --workspace --all-targets -- -D warnings    0 warnings

Test plan

  1. Start a listener that replies with a non-canonical header and a valid two-chunk body:
printf 'HTTP/1.1 200 OK\r\nTransfer-Encoding:chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n' | nc -l 8099
  1. Call probe_http_get_body("127.0.0.1:8099", "/", false, ProbeTimeouts::default()).
  2. On main the returned String carries the chunk-size lines; on this branch it is hello world.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of HTTP response bodies when using Transfer-Encoding.
    • More robust chunked decoding now supports optional whitespace, mixed casing, and repeated Transfer-Encoding header values (with chunking determined only by the final coding).
    • Added safer behavior for unsupported transfer codings (e.g., rejecting gzip, chunked instead of returning corrupted text).
  • Tests
    • Expanded HTTP body decoding coverage, including non-canonical header formatting and new validation cases.

GeniePod#910)

read_http_body detected chunk framing by comparing the entire header line
against "transfer-encoding: chunked", so it only matched the single
canonical spelling. RFC 7230 3.2 allows zero or more spaces after the
colon and 3.3.1 permits a codings list whose last entry frames the body,
so Transfer-Encoding:chunked and gzip, chunked both slipped past.

a chunked response carries no Content-Length, so the miss fell through to
the read-to-EOF path and returned the wire bytes verbatim — the caller
got a 200 and a body with the chunk-size lines inlined
("5\r\nhello\r\n6\r\n world\r\n0" instead of "hello world"), with nothing
surfacing an error.

is_chunked now parses the header the way parse_content_length two
functions below already did: split on the colon, compare the name
case-insensitively, and require the final comma-separated coding to be
chunked.
@github-actions github-actions Bot added the bug Something isn't working label Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e1149ae-6a1a-49ee-b5d6-a5a7924f7bfe

📥 Commits

Reviewing files that changed from the base of the PR and between 4ba5f26 and 48620df.

📒 Files selected for processing (1)
  • crates/genie-common/src/probe.rs

📝 Walkthrough

Walkthrough

Updated HTTP body reading to parse Transfer-Encoding fields according to their coding lists and received order. Chunked decoding now depends on the final coding, while unsupported codings are rejected. Tests cover spacing, repeated fields, ordering, and supported or rejected combinations.

Changes

Transfer-Encoding handling

Layer / File(s) Summary
Transfer-Encoding parsing, validation, and regression coverage
crates/genie-common/src/probe.rs
read_http_body rejects unsupported transfer codings before decoding. New helpers parse repeated and comma-separated fields, and is_chunked recognizes chunked only as the final coding. Tests cover non-canonical spacing, field ordering, and gzip, chunked versus identity, chunked.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bug

Suggested reviewers: minion1227

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: parsing Transfer-Encoding instead of matching the full header line.
Linked Issues check ✅ Passed The changes address #910 by parsing Transfer-Encoding case-insensitively, handling repeated fields, and only treating final chunked as chunk framing.
Out of Scope Changes check ✅ Passed The added rejection of unsupported codings and extra regression tests are directly tied to the reported Transfer-Encoding bug.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/genie-common/src/probe.rs`:
- Around line 746-776: Update the HTTP body handling exercised by
probe_http_get_body/read_http_body so a response declaring “gzip, chunked” is
either decoded through the supported transfer codings before producing a String
or explicitly rejected as unsupported. Replace the current invalid
fixture/assertion with behavior that uses valid gzip-compressed chunk contents
or verifies the rejection path, while preserving the existing plain chunked
cases.
- Around line 420-429: Update the Transfer-Encoding detection logic in the
surrounding probe function to combine all repeated Transfer-Encoding field
values in header order, then inspect only the final coding for chunked framing
instead of using .any(...). Ensure an earlier chunked value followed by another
coding is treated as close-delimited, and add coverage for repeated fields whose
final coding is not chunked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ebd0e79-f93c-43ba-ae18-ff92eadcaa30

📥 Commits

Reviewing files that changed from the base of the PR and between 02a577d and 4ba5f26.

📒 Files selected for processing (1)
  • crates/genie-common/src/probe.rs

Comment thread crates/genie-common/src/probe.rs Outdated
Comment thread crates/genie-common/src/probe.rs Outdated
addresses both review findings on GeniePod#911, which were flaws in that patch
rather than in the original code.

is_chunked used .any() over the header lines, so an earlier
`Transfer-Encoding: chunked` won even when a later field changed the
combined value. repeated fields combine in order as one list (RFC 9110
5.3) and only the final coding frames the body (RFC 9112 6.1), so
`chunked` followed by `gzip` is `chunked, gzip` — close-delimited, not
chunk-framed. transfer_codings now collects the codings in order and
is_chunked asks only whether the last one is chunked.

the `gzip, chunked` case in the test was also wrong: it shipped
uncompressed bytes inside the chunks and asserted plain text came back.
on a conformant response those bytes are gzip, and read_http_body only
removes chunk framing before a lossy utf-8 conversion — so the caller
would get a plausible-looking String that is silently mangled. rather
than add a gzip decoder, undecodable_codings reports any coding other
than a trailing chunked (identity excepted) and read_http_body fails
with "unsupported transfer coding". mirrors the inbound reader in
http.rs, which already rejects transfer-encodings it cannot handle.

the fixture is replaced by two tests: one pinning the ordering contract
over repeated fields, one asserting `gzip, chunked` is refused while
`identity, chunked` still decodes. both fail on the previous commit —
the second returned "hello world" for a body that was never plain text.
@atoz96

atoz96 commented Jul 30, 2026

Copy link
Copy Markdown
Author

Both findings were right, and both were flaws in my patch rather than in the original code. Fixed in 48620df.

1. Final coding, not any coding. is_chunked used .any() across header lines, so an earlier Transfer-Encoding: chunked won even when a later field changed the combined value. Repeated fields combine in order as a single list (RFC 9110 §5.3) and only the final coding frames the body (RFC 9112 §6.1) — so chunked then gzip is chunked, gzip, which is close-delimited, and my code would have tried to chunk-parse it. transfer_codings now collects codings in order and is_chunked asks only whether the last is chunked.

2. The gzip, chunked fixture was invalid. It shipped uncompressed bytes inside the chunks and asserted plain text came back. You are right that on a conformant response those bytes are gzip, and read_http_body only strips chunk framing before a lossy UTF-8 conversion — so the caller gets a plausible-looking String that is silently mangled, which is worse than an error.

Rather than pull in a gzip decoder, I took the reject path you offered as the alternative: undecodable_codings reports any coding other than a trailing chunked (identity excepted, since it is a no-op), and read_http_body fails with unsupported transfer coding: …. That matches the posture already in http.rs, whose inbound reader returns UnsupportedTransferEncoding for transfer-encodings it cannot handle — so the crate now answers the same way on both directions.

The bad fixture is gone, replaced by:

  • repeated_transfer_encoding_fields_combine_in_order — pins the ordering contract, including the repeated-fields case whose final coding is not chunked, and the no-Transfer-Encoding case
  • undecodable_transfer_coding_is_rejected_not_mangled — asserts gzip, chunked is refused, and that identity, chunked still decodes so the guard is not over-broad

The two plain non-canonical spellings (Transfer-Encoding:chunked, Transfer-Encoding: chunked) are preserved as you asked.

Both new tests fail on the previous commit. The second is the clearest statement of the bug you found:

called `Result::unwrap_err()` on an `Ok` value: "hello world"

— the old code cheerfully returned "hello world" for a response whose payload was never plain text.

cargo test --workspace                                   no failures (genie-common 140 passed)
cargo fmt --all -- --check                               clean
cargo clippy --workspace --all-targets -- -D warnings    0 warnings

Verification path is unchanged from the PR body: x86_64 laptop, no Jetson — this is pure HTTP framing driven through a real loopback TcpListener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] chunked response body returned with its chunk framing when Transfer-Encoding is non-canonical

1 participant