mirror of https://github.com/fafhrd91/actix-web
fix(http): preserve manual Content-Length headers for HEAD requests per RFC 7230
Implements RFC 7230 Section 3.3.2 which allows servers to send Content-Length headers in HEAD responses. This enables support for APIs like AWS S3 HeadObject that require Content-Length headers even with empty response bodies. Previously, manually set Content-Length headers on empty body responses were being overridden by automatic "content-length: 0" headers. Now the encoder checks for existing Content-Length headers before adding automatic ones.
This commit is contained in:
parent
351f7a17ed
commit
7dc772c96d
|
@ -60,8 +60,13 @@ pub(crate) trait MessageType: Sized {
|
|||
config: &ServiceConfig,
|
||||
) -> io::Result<()> {
|
||||
let chunked = self.chunked();
|
||||
let mut skip_len = length != BodySize::Stream;
|
||||
let mut skip_len = false;
|
||||
let camel_case = self.camel_case();
|
||||
// Check if Content-Length header is already set
|
||||
let has_content_length = self.headers().contains_key(CONTENT_LENGTH)
|
||||
|| self
|
||||
.extra_headers()
|
||||
.is_some_and(|h| h.contains_key(CONTENT_LENGTH));
|
||||
|
||||
// Content length
|
||||
if let Some(status) = self.status() {
|
||||
|
@ -103,6 +108,7 @@ pub(crate) trait MessageType: Sized {
|
|||
dst.put_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
BodySize::Sized(_len) if skip_len || has_content_length => dst.put_slice(b"\r\n"),
|
||||
BodySize::Sized(0) if camel_case => dst.put_slice(b"\r\nContent-Length: 0\r\n"),
|
||||
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
|
||||
BodySize::Sized(len) => helpers::write_content_length(len, dst, camel_case),
|
||||
|
@ -668,4 +674,51 @@ mod tests {
|
|||
assert!(!data.contains("content-length: 0\r\n"));
|
||||
assert!(!data.contains("transfer-encoding: chunked\r\n"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_manual_content_length_preserved() {
|
||||
let mut bytes = BytesMut::with_capacity(2048);
|
||||
|
||||
// Test with OK response and manual Content-Length header
|
||||
let mut res = Response::with_body(StatusCode::OK, ());
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("456"));
|
||||
|
||||
let _ = res.encode_headers(
|
||||
&mut bytes,
|
||||
Version::HTTP_11,
|
||||
BodySize::Sized(0), // Empty body
|
||||
ConnectionType::KeepAlive,
|
||||
&ServiceConfig::default(),
|
||||
);
|
||||
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||
|
||||
// Should only have the manual Content-Length: 456, not an automatic content-length: 0
|
||||
assert!(data.contains("content-length: 456\r\n"));
|
||||
assert!(!data.contains("content-length: 0\r\n"));
|
||||
|
||||
// Count occurrences of "content-length" - should be exactly 1
|
||||
let count = data.matches("content-length").count();
|
||||
assert_eq!(count, 1, "Should have exactly one content-length header");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_automatic_content_length_for_empty_body() {
|
||||
let mut bytes = BytesMut::with_capacity(2048);
|
||||
|
||||
// Test without manual Content-Length header
|
||||
let mut res = Response::with_body(StatusCode::OK, ());
|
||||
|
||||
let _ = res.encode_headers(
|
||||
&mut bytes,
|
||||
Version::HTTP_11,
|
||||
BodySize::Sized(0), // Empty body
|
||||
ConnectionType::KeepAlive,
|
||||
&ServiceConfig::default(),
|
||||
);
|
||||
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||
|
||||
// Should have automatic content-length: 0
|
||||
assert!(data.contains("content-length: 0\r\n"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue