Skip to content

Commit 2580f11

Browse files
committed
Fix manual framing incorrectly falling back to automatic mode
When using manual framing with only a content-length or transfer encoding header set, viceroy was falling back to auto mode
1 parent 497e50b commit 2580f11

File tree

5 files changed

+78
-20
lines changed

5 files changed

+78
-20
lines changed

cli/tests/integration/http_semantics.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,32 @@ viceroy_test!(framing_headers_are_overridden, |is_component| {
3939
Ok(())
4040
});
4141

42+
// Regression test: ManuallyFromHeaders mode should preserve Content-Length header.
43+
// Previously, the header was stripped because absent Transfer-Encoding was treated as invalid.
44+
viceroy_test!(manual_framing_preserves_content_length, |is_component| {
45+
let test = Test::using_fixture("manual-framing-headers.wasm")
46+
.adapt_component(is_component)
47+
.backend("TheOrigin", "/", None, |req| {
48+
// Manual mode should preserve Content-Length, not strip it
49+
assert!(
50+
!req.headers().contains_key(header::CONTENT_LENGTH),
51+
"Content-Length was incorrectly added"
52+
);
53+
// Transfer-Encoding should be preserved
54+
assert_eq!(
55+
req.headers().get(header::TRANSFER_ENCODING),
56+
Some(&hyper::header::HeaderValue::from_static("chunked")),
57+
"Transfer-Encoding should have been preserved"
58+
);
59+
Response::new(Vec::new())
60+
})
61+
.await;
62+
63+
let resp = test.via_hyper().against_empty().await?;
64+
assert_eq!(resp.status(), StatusCode::OK);
65+
Ok(())
66+
});
67+
4268
viceroy_test!(content_length_is_computed_correctly, |is_component| {
4369
// Set up the test harness
4470
let test = Test::using_fixture("content-length.wasm")

src/framing.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,22 @@ use http::{header, HeaderMap};
55
pub fn content_length_is_valid(headers: &HeaderMap) -> bool {
66
let mut values = headers.get_all(header::CONTENT_LENGTH).iter();
77

8-
if let Some(val) = values.next() {
9-
if val.as_bytes().iter().all(|b| b.is_ascii_digit()) && values.next().is_none() {
10-
return true;
11-
}
8+
match values.next() {
9+
None => true,
10+
Some(val) => val.as_bytes().iter().all(|b| b.is_ascii_digit()) && values.next().is_none(),
1211
}
13-
false
1412
}
1513

1614
pub fn transfer_encoding_is_supported(headers: &HeaderMap) -> bool {
1715
let mut values = headers.get_all(header::TRANSFER_ENCODING).iter();
1816

19-
if let Some(val) = values.next() {
20-
if val
21-
.to_str()
22-
.map(|s| s.eq_ignore_ascii_case("chunked"))
23-
.unwrap_or(false)
24-
&& values.next().is_none()
25-
{
26-
return true;
17+
match values.next() {
18+
None => true,
19+
Some(val) => {
20+
val.to_str()
21+
.map(|s| s.eq_ignore_ascii_case("chunked"))
22+
.unwrap_or(false)
23+
&& values.next().is_none()
2724
}
2825
}
29-
false
3026
}

src/session/downstream.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,20 @@ impl DownstreamResponseState {
6767

6868
if framing_headers_mode == FramingHeadersMode::ManuallyFromHeaders {
6969
if !content_length_is_valid(response.headers()) {
70-
tracing::warn!("Downstream response has invalid Content-Length header, falling back to automatic framing.");
70+
tracing::warn!("Downstream response has malformed Content-Length header, falling back to automatic framing.");
7171
framing_headers_mode = FramingHeadersMode::Automatic;
72-
}
73-
if !transfer_encoding_is_supported(response.headers()) {
72+
} else if !transfer_encoding_is_supported(response.headers()) {
7473
tracing::warn!("Downstream response has unsupported Transfer-Encoding header, falling back to automatic framing.");
7574
framing_headers_mode = FramingHeadersMode::Automatic;
75+
} else if !response
76+
.headers()
77+
.contains_key(hyper::header::CONTENT_LENGTH)
78+
&& !response
79+
.headers()
80+
.contains_key(hyper::header::TRANSFER_ENCODING)
81+
{
82+
tracing::warn!("Downstream response has neither Content-Length nor Transfer-Encoding header, falling back to automatic framing.");
83+
framing_headers_mode = FramingHeadersMode::Automatic;
7684
}
7785
}
7886
if framing_headers_mode != FramingHeadersMode::ManuallyFromHeaders {

src/upstream.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,12 +316,16 @@ pub fn send_request(
316316

317317
if framing_headers_mode == FramingHeadersMode::ManuallyFromHeaders {
318318
if !content_length_is_valid(req.headers()) {
319-
warn!("Backend request has invalid Content-Length header, falling back to automatic framing.");
319+
warn!("Backend request has malformed Content-Length header, falling back to automatic framing.");
320320
framing_headers_mode = FramingHeadersMode::Automatic;
321-
}
322-
if !transfer_encoding_is_supported(req.headers()) {
321+
} else if !transfer_encoding_is_supported(req.headers()) {
323322
warn!("Backend request has unsupported Transfer-Encoding header, falling back to automatic framing.");
324323
framing_headers_mode = FramingHeadersMode::Automatic;
324+
} else if !req.headers().contains_key(header::CONTENT_LENGTH)
325+
&& !req.headers().contains_key(header::TRANSFER_ENCODING)
326+
{
327+
warn!("Backend request has neither Content-Length nor Transfer-Encoding header, falling back to automatic framing.");
328+
framing_headers_mode = FramingHeadersMode::Automatic;
325329
}
326330
}
327331
if framing_headers_mode != FramingHeadersMode::ManuallyFromHeaders {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! Test that ManuallyFromHeaders mode preserves Transfer-Encoding header.
2+
//!
3+
//! With manual framing mode, the guest explicitly sets framing headers (Content-Length
4+
//! or Transfer-Encoding) and they should be preserved rather than stripped.
5+
//!
6+
//! We use a streaming body so hyper doesn't know the length upfront.
7+
8+
use fastly::http::{header, FramingHeadersMode, HeaderValue};
9+
use fastly::{Error, Request, Response};
10+
use std::io::Write;
11+
12+
fn main() -> Result<(), Error> {
13+
let (mut stream, pending) = Request::post("http://example.org/")
14+
.with_header(header::TRANSFER_ENCODING, HeaderValue::from_static("chunked"))
15+
.with_framing_headers_mode(FramingHeadersMode::ManuallyFromHeaders)
16+
.send_async_streaming("TheOrigin")?;
17+
18+
write!(stream, "test")?;
19+
stream.finish()?;
20+
pending.wait()?;
21+
22+
Response::new().send_to_client();
23+
Ok(())
24+
}

0 commit comments

Comments
 (0)