use std::fmt;
use super::body::BodySource;
use super::header_block::{HeaderBlock, HeaderError, HeaderName, HeaderValue};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseConstructionError {
InvalidStatus(u16),
InvalidHeader(HeaderError),
ForbiddenFramingHeader(String),
BodyAlreadyConsumed,
ContentLengthMismatch { declared: u64, actual: u64 },
FileStreamLimit,
}
impl fmt::Display for ResponseConstructionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidStatus(code) => write!(f, "invalid status code: {}", code),
Self::InvalidHeader(e) => write!(f, "invalid header: {}", e),
Self::ForbiddenFramingHeader(name) => {
write!(f, "forbidden framing header: {}", name)
}
Self::BodyAlreadyConsumed => write!(f, "response body already consumed"),
Self::ContentLengthMismatch { declared, actual } => {
write!(
f,
"content-length mismatch: declared {}, actual {}",
declared, actual
)
}
Self::FileStreamLimit => write!(f, "file stream admission limit reached"),
}
}
}
impl std::error::Error for ResponseConstructionError {}
impl From<HeaderError> for ResponseConstructionError {
fn from(e: HeaderError) -> Self {
Self::InvalidHeader(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StatusCode(u16);
impl StatusCode {
pub const CONTINUE: Self = Self(100);
pub const SWITCHING_PROTOCOLS: Self = Self(101);
pub const OK: Self = Self(200);
pub const CREATED: Self = Self(201);
pub const NO_CONTENT: Self = Self(204);
pub const RESET_CONTENT: Self = Self(205);
pub const NOT_MODIFIED: Self = Self(304);
pub const MOVED_PERMANENTLY: Self = Self(301);
pub const BAD_REQUEST: Self = Self(400);
pub const FORBIDDEN: Self = Self(403);
pub const NOT_FOUND: Self = Self(404);
pub const METHOD_NOT_ALLOWED: Self = Self(405);
pub const REQUEST_TIMEOUT: Self = Self(408);
pub const PAYLOAD_TOO_LARGE: Self = Self(413);
pub const RANGE_NOT_SATISFIABLE: Self = Self(416);
pub const INTERNAL_SERVER_ERROR: Self = Self(500);
pub const SERVICE_UNAVAILABLE: Self = Self(503);
pub fn new(code: u16) -> Result<Self, ResponseConstructionError> {
if !(100..=599).contains(&code) {
return Err(ResponseConstructionError::InvalidStatus(code));
}
Ok(Self(code))
}
pub fn as_u16(&self) -> u16 {
self.0
}
pub fn is_informational(&self) -> bool {
(100..200).contains(&self.0)
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.0)
}
pub fn is_redirection(&self) -> bool {
(300..400).contains(&self.0)
}
pub fn is_client_error(&self) -> bool {
(400..500).contains(&self.0)
}
pub fn is_server_error(&self) -> bool {
(500..600).contains(&self.0)
}
pub fn permits_payload_body(&self) -> bool {
!self.is_informational() && self.0 != 204 && self.0 != 205 && self.0 != 304
}
}
impl fmt::Display for StatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<StatusCode> for u16 {
fn from(s: StatusCode) -> u16 {
s.0
}
}
#[derive(Debug, Clone)]
pub struct ResponseHead {
status: StatusCode,
headers: HeaderBlock,
}
impl ResponseHead {
pub fn new(status: StatusCode, headers: HeaderBlock) -> Self {
Self { status, headers }
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn headers(&self) -> &HeaderBlock {
&self.headers
}
pub fn headers_mut(&mut self) -> &mut HeaderBlock {
&mut self.headers
}
}
#[derive(Debug)]
pub enum ResponseBody {
Empty,
Bytes(Vec<u8>),
File(BodySource),
EmptyWithLength(u64),
}
impl ResponseBody {
pub fn len(&self) -> u64 {
match self {
Self::Empty => 0,
Self::Bytes(b) => b.len() as u64,
Self::File(source) => source.len(),
Self::EmptyWithLength(len) => *len,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn into_bytes(self) -> Option<Vec<u8>> {
match self {
Self::Empty => None,
Self::Bytes(b) => Some(b),
Self::File(_) => None,
Self::EmptyWithLength(_) => None,
}
}
}
pub struct Response {
head: ResponseHead,
body: Option<ResponseBody>,
}
impl Response {
pub fn builder() -> ResponseBuilder {
ResponseBuilder {
status: None,
headers: HeaderBlock::new(),
}
}
pub fn head(&self) -> &ResponseHead {
&self.head
}
pub fn head_mut(&mut self) -> &mut ResponseHead {
&mut self.head
}
pub fn status(&self) -> StatusCode {
self.head.status()
}
pub fn headers(&self) -> &HeaderBlock {
self.head.headers()
}
pub fn take_body(&mut self) -> Option<ResponseBody> {
self.body.take()
}
pub fn body(&self) -> Option<&ResponseBody> {
self.body.as_ref()
}
}
impl fmt::Debug for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Response")
.field("head", &self.head)
.field("body", &self.body)
.finish()
}
}
pub struct ResponseBuilder {
status: Option<StatusCode>,
headers: HeaderBlock,
}
impl ResponseBuilder {
pub fn status(mut self, status: StatusCode) -> Self {
self.status = Some(status);
self
}
pub fn push_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.headers.push(name, value);
self
}
pub fn header(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, ResponseConstructionError> {
let name = HeaderName::new(name)?;
let value = HeaderValue::new(value)?;
self.headers.push(name, value);
Ok(self)
}
pub fn body(self, body: ResponseBody) -> Result<Response, ResponseConstructionError> {
let status = self
.status
.ok_or(ResponseConstructionError::InvalidStatus(0))?;
Ok(Response {
head: ResponseHead::new(status, self.headers),
body: Some(body),
})
}
pub fn empty(self) -> Result<Response, ResponseConstructionError> {
self.body(ResponseBody::Empty)
}
}
pub struct NormalizeRequest {
pub is_head: bool,
}
impl NormalizeRequest {
pub fn new(is_head: bool) -> Self {
Self { is_head }
}
}
pub fn normalize_response(
mut response: Response,
request: &NormalizeRequest,
) -> Result<Response, ResponseConstructionError> {
let status = response.status();
let body_len = response.body.as_ref().map_or(0, |b| b.len());
if request.is_head {
response.body = Some(ResponseBody::Empty);
}
if !status.permits_payload_body() {
response.body = Some(ResponseBody::Empty);
}
normalize_metadata(
status,
response.head.headers_mut(),
body_len,
request.is_head,
)?;
Ok(response)
}
pub fn normalize_metadata(
status: StatusCode,
headers: &mut HeaderBlock,
body_len: u64,
is_head: bool,
) -> Result<(), ResponseConstructionError> {
strip_hop_by_hop(headers);
let not_modified_length = if status == StatusCode::NOT_MODIFIED {
headers
.get_unique("content-length")
.ok()
.flatten()
.and_then(|value| value.as_str().parse::<u64>().ok())
.filter(|length| *length == body_len)
} else {
None
};
remove_header(headers, "content-length");
if (status.permits_payload_body() && !(is_head && body_len == 0))
|| not_modified_length.is_some()
{
let length = not_modified_length.unwrap_or(body_len);
headers
.push_str("content-length", length.to_string())
.map_err(ResponseConstructionError::from)?;
}
Ok(())
}
pub fn is_hop_by_hop_header(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"connection"
| "keep-alive"
| "proxy-authenticate"
| "proxy-authorization"
| "proxy-connection"
| "te"
| "trailer"
| "transfer-encoding"
| "upgrade"
)
}
fn remove_header(headers: &mut HeaderBlock, name: &str) {
let lower = name.to_ascii_lowercase();
headers.retain(|f| f.name.as_str().to_ascii_lowercase() != lower);
}
fn strip_hop_by_hop(headers: &mut HeaderBlock) {
headers.retain(|f| !is_hop_by_hop_header(f.name.as_str()));
}
pub fn to_hyper_response(
response: Response,
) -> Result<
hyper::Response<http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error>>,
ResponseConstructionError,
> {
to_hyper_response_with_optional_file_stream_semaphore(response, None)
}
pub fn to_hyper_response_with_file_stream_semaphore(
response: Response,
semaphore: &std::sync::Arc<tokio::sync::Semaphore>,
) -> Result<
hyper::Response<http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error>>,
ResponseConstructionError,
> {
to_hyper_response_with_optional_file_stream_semaphore(response, Some(semaphore))
}
fn to_hyper_response_with_optional_file_stream_semaphore(
response: Response,
semaphore: Option<&std::sync::Arc<tokio::sync::Semaphore>>,
) -> Result<
hyper::Response<http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error>>,
ResponseConstructionError,
> {
use bytes::Bytes;
use http_body_util::BodyExt;
use http_body_util::Full;
let status = response.status();
let code = status.as_u16();
let hyper_status = hyper::StatusCode::from_u16(code)
.map_err(|_| ResponseConstructionError::InvalidStatus(code))?;
let mut builder = hyper::Response::builder().status(hyper_status);
for field in response.head.headers().iter() {
builder = builder.header(field.name.as_str(), field.value.as_str());
}
let body = match response.body {
Some(ResponseBody::Empty) => Full::new(Bytes::new())
.map_err(|never| match never {})
.boxed(),
Some(ResponseBody::Bytes(b)) => Full::new(Bytes::from(b))
.map_err(|never| match never {})
.boxed(),
Some(ResponseBody::File(source)) => {
let permit = semaphore
.map(|s| s.clone().try_acquire_owned())
.transpose()
.map_err(|_| ResponseConstructionError::FileStreamLimit)?;
let permit = permit.map(CountingFileStreamPermit::new);
file_body(source, permit)
}
Some(ResponseBody::EmptyWithLength(_)) => Full::new(Bytes::new())
.map_err(|never| match never {})
.boxed(),
None => Full::new(Bytes::new())
.map_err(|never| match never {})
.boxed(),
};
let mut response = builder
.body(body)
.map_err(|_| ResponseConstructionError::InvalidHeader(HeaderError::InvalidValue))?;
crate::response::finalize_origin_headers(&mut response, std::time::SystemTime::now());
Ok(response)
}
fn file_body(
source: BodySource,
permit: Option<CountingFileStreamPermit>,
) -> http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error> {
use bytes::Bytes;
use futures_util::stream;
use http_body_util::{BodyExt, StreamBody};
use hyper::body::Frame;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
let (file, start, remaining) = match source {
BodySource::FileFull { file, len, .. } => (tokio::fs::File::from_std(file), 0, len),
BodySource::FileRange { file, range, .. } => {
(tokio::fs::File::from_std(file), range.start, range.len())
}
BodySource::Empty => {
return http_body_util::Full::new(Bytes::new())
.map_err(|never| match never {})
.boxed();
}
BodySource::Bytes(bytes) => {
return http_body_util::Full::new(Bytes::from(bytes))
.map_err(|never| match never {})
.boxed();
}
};
let stream = stream::unfold(
(file, start, remaining, permit),
|(mut file, offset, remaining, permit)| async move {
if remaining == 0 {
return None;
}
if offset > 0 {
if let Err(error) = file.seek(std::io::SeekFrom::Start(offset)).await {
return Some((Err(error), (file, offset, 0, permit)));
}
}
let chunk_len = remaining.min(64 * 1024) as usize;
let mut buffer = vec![0; chunk_len];
match file.read_exact(&mut buffer).await {
Ok(_) => Some((
Ok(Frame::data(Bytes::from(buffer))),
(
file,
offset + chunk_len as u64,
remaining - chunk_len as u64,
permit,
),
)),
Err(error) => Some((Err(error), (file, offset, 0, permit))),
}
},
);
StreamBody::new(stream).boxed()
}
struct CountingFileStreamPermit {
_permit: tokio::sync::OwnedSemaphorePermit,
}
impl CountingFileStreamPermit {
fn new(permit: tokio::sync::OwnedSemaphorePermit) -> Self {
crate::ops::global_counters()
.active_file_streams
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Self { _permit: permit }
}
}
impl Drop for CountingFileStreamPermit {
fn drop(&mut self) {
crate::ops::global_counters()
.active_file_streams
.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::FileRange;
use http_body_util::BodyExt;
use std::fs::File;
use std::sync::Arc;
use tempfile::TempDir;
fn file_response(path: &std::path::Path, range: Option<FileRange>) -> Response {
let file = File::open(path).unwrap();
let metadata = file.metadata().unwrap();
let source = match range {
Some(range) => BodySource::FileRange {
file,
range,
total_len: metadata.len(),
mime: "application/octet-stream",
},
None => BodySource::FileFull {
file,
len: metadata.len(),
mime: "application/octet-stream",
},
};
Response::builder()
.status(StatusCode::OK)
.body(ResponseBody::File(source))
.unwrap()
}
#[tokio::test]
async fn full_file_transport_body_owns_permit_until_drop() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("full.bin");
std::fs::write(&path, b"full body").unwrap();
let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
let first =
to_hyper_response_with_file_stream_semaphore(file_response(&path, None), &semaphore)
.unwrap();
assert!(matches!(
to_hyper_response_with_file_stream_semaphore(file_response(&path, None), &semaphore),
Err(ResponseConstructionError::FileStreamLimit)
));
drop(first);
assert!(to_hyper_response_with_file_stream_semaphore(
file_response(&path, None),
&semaphore
)
.is_ok());
}
#[tokio::test]
async fn range_file_transport_body_releases_permit_on_completion() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("range.bin");
std::fs::write(&path, b"range body").unwrap();
let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
let response = to_hyper_response_with_file_stream_semaphore(
file_response(&path, Some(FileRange::new(0, 4))),
&semaphore,
)
.unwrap();
let body = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&body[..], b"range");
assert!(to_hyper_response_with_file_stream_semaphore(
file_response(&path, Some(FileRange::new(5, 9))),
&semaphore
)
.is_ok());
}
#[test]
fn non_file_and_normalized_head_bodies_bypass_file_admission() {
let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
let held = semaphore.clone().try_acquire_owned().unwrap();
for body in [
ResponseBody::Bytes(b"bytes".to_vec()),
ResponseBody::Empty,
ResponseBody::EmptyWithLength(5),
] {
let response = Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap();
assert!(to_hyper_response_with_file_stream_semaphore(response, &semaphore).is_ok());
}
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("head.bin");
std::fs::write(&path, b"head body").unwrap();
let normalized =
normalize_response(file_response(&path, None), &NormalizeRequest::new(true)).unwrap();
assert!(to_hyper_response_with_file_stream_semaphore(normalized, &semaphore).is_ok());
drop(held);
}
#[test]
fn status_code_valid_range() {
assert!(StatusCode::new(100).is_ok());
assert!(StatusCode::new(200).is_ok());
assert!(StatusCode::new(600).is_err());
}
#[test]
fn status_code_zero_rejected() {
assert!(StatusCode::new(0).is_err());
}
#[test]
fn status_code_below_100_rejected() {
assert!(StatusCode::new(1).is_err());
assert!(StatusCode::new(42).is_err());
assert!(StatusCode::new(99).is_err());
}
#[test]
fn status_code_over_599_rejected() {
assert!(StatusCode::new(600).is_err());
assert!(StatusCode::new(1000).is_err());
}
#[test]
fn status_code_boundary_values() {
assert!(StatusCode::new(100).is_ok());
assert!(StatusCode::new(199).is_ok());
assert!(StatusCode::new(200).is_ok());
assert!(StatusCode::new(599).is_ok());
}
#[test]
fn status_code_classification() {
assert!(StatusCode::CONTINUE.is_informational());
assert!(!StatusCode::OK.is_informational());
assert!(StatusCode::OK.is_success());
assert!(StatusCode::NOT_MODIFIED.is_redirection());
assert!(StatusCode::BAD_REQUEST.is_client_error());
assert!(StatusCode::INTERNAL_SERVER_ERROR.is_server_error());
}
#[test]
fn status_code_permits_payload() {
assert!(!StatusCode::CONTINUE.permits_payload_body());
assert!(!StatusCode::NO_CONTENT.permits_payload_body());
assert!(!StatusCode::NOT_MODIFIED.permits_payload_body());
assert!(!StatusCode::new(205).unwrap().permits_payload_body());
assert!(StatusCode::OK.permits_payload_body());
assert!(StatusCode::RANGE_NOT_SATISFIABLE.permits_payload_body());
}
#[test]
fn response_body_len() {
assert_eq!(ResponseBody::Empty.len(), 0);
assert_eq!(ResponseBody::Bytes(b"hello".to_vec()).len(), 5);
}
#[test]
fn response_body_into_bytes() {
assert!(ResponseBody::Empty.into_bytes().is_none());
assert_eq!(
ResponseBody::Bytes(b"hi".to_vec()).into_bytes(),
Some(b"hi".to_vec())
);
}
#[test]
fn response_builder_creates_response() {
let resp = Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/plain")
.unwrap()
.body(ResponseBody::Bytes(b"ok".to_vec()))
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.headers().get_first("content-type").unwrap().as_str(),
"text/plain"
);
}
#[test]
fn response_builder_empty_body() {
let resp = Response::builder()
.status(StatusCode::NO_CONTENT)
.empty()
.unwrap();
assert_eq!(resp.status().as_u16(), 204);
assert!(resp.body().unwrap().is_empty());
}
#[test]
fn response_builder_no_status_returns_error() {
let result = Response::builder()
.header("content-type", "text/plain")
.unwrap()
.empty();
assert!(result.is_err());
}
#[test]
fn response_builder_invalid_header_name_rejected() {
let result = Response::builder()
.status(StatusCode::OK)
.header("", "value");
assert!(result.is_err());
}
#[test]
fn response_builder_invalid_header_value_rejected() {
let result = Response::builder()
.status(StatusCode::OK)
.header("x-test", "val\r\ninjection");
assert!(result.is_err());
}
#[test]
fn normalize_head_suppresses_body() {
let resp = Response::builder()
.status(StatusCode::OK)
.header("content-length", "5")
.unwrap()
.body(ResponseBody::Bytes(b"hello".to_vec()))
.unwrap();
let req = NormalizeRequest::new(true);
let normalized = normalize_response(resp, &req).unwrap();
assert!(normalized.body().unwrap().is_empty());
}
#[test]
fn normalize_304_suppresses_body() {
let resp = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("etag", "W/\"123\"")
.unwrap()
.body(ResponseBody::Empty)
.unwrap();
let req = NormalizeRequest::new(false);
let normalized = normalize_response(resp, &req).unwrap();
assert_eq!(normalized.status().as_u16(), 304);
assert!(normalized.body().unwrap().is_empty());
}
#[test]
fn normalize_304_preserves_only_matching_content_length() {
let matching = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("content-length", "5")
.unwrap()
.body(ResponseBody::Bytes(b"hello".to_vec()))
.unwrap();
let normalized = normalize_response(matching, &NormalizeRequest::new(false)).unwrap();
assert_eq!(
normalized
.headers()
.get_first("content-length")
.unwrap()
.as_str(),
"5"
);
let mismatched = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("content-length", "4")
.unwrap()
.body(ResponseBody::Bytes(b"hello".to_vec()))
.unwrap();
let normalized = normalize_response(mismatched, &NormalizeRequest::new(false)).unwrap();
assert!(!normalized.headers().contains("content-length"));
}
#[test]
fn normalize_204_suppresses_body() {
let resp = Response::builder()
.status(StatusCode::NO_CONTENT)
.body(ResponseBody::Bytes(b"unexpected".to_vec()))
.unwrap();
let req = NormalizeRequest::new(false);
let normalized = normalize_response(resp, &req).unwrap();
assert!(normalized.body().unwrap().is_empty());
}
#[test]
fn normalize_205_suppresses_body_and_content_length() {
let resp = Response::builder()
.status(StatusCode::RESET_CONTENT)
.body(ResponseBody::Bytes(b"unexpected".to_vec()))
.unwrap();
let normalized = normalize_response(resp, &NormalizeRequest::new(false)).unwrap();
assert!(normalized.body().unwrap().is_empty());
assert!(!normalized.headers().contains("content-length"));
}
#[test]
fn normalize_strips_transfer_encoding() {
let resp = Response::builder()
.status(StatusCode::OK)
.header("transfer-encoding", "chunked")
.unwrap()
.body(ResponseBody::Bytes(b"hello".to_vec()))
.unwrap();
let req = NormalizeRequest::new(false);
let normalized = normalize_response(resp, &req).unwrap();
assert!(!normalized.headers().contains("transfer-encoding"));
}
#[test]
fn normalize_sets_content_length() {
let resp = Response::builder()
.status(StatusCode::OK)
.body(ResponseBody::Bytes(b"hello".to_vec()))
.unwrap();
let req = NormalizeRequest::new(false);
let normalized = normalize_response(resp, &req).unwrap();
assert_eq!(
normalized
.headers()
.get_first("content-length")
.unwrap()
.as_str(),
"5"
);
}
#[test]
fn normalize_1xx_suppresses_body() {
let resp = Response::builder()
.status(StatusCode::CONTINUE)
.body(ResponseBody::Bytes(b"data".to_vec()))
.unwrap();
let req = NormalizeRequest::new(false);
let normalized = normalize_response(resp, &req).unwrap();
assert!(normalized.body().unwrap().is_empty());
}
#[test]
fn normalize_duplicate_headers_preserved() {
let mut resp = Response::builder()
.status(StatusCode::OK)
.body(ResponseBody::Bytes(b"ok".to_vec()))
.unwrap();
resp.head.headers.push_str("set-cookie", "a=1").unwrap();
resp.head.headers.push_str("set-cookie", "b=2").unwrap();
let req = NormalizeRequest::new(false);
let normalized = normalize_response(resp, &req).unwrap();
let all = normalized.headers().get_all("set-cookie");
assert_eq!(all.len(), 2);
}
#[test]
fn response_construction_error_display() {
let err = ResponseConstructionError::InvalidStatus(0);
assert!(err.to_string().contains("0"));
let err = ResponseConstructionError::ForbiddenFramingHeader("transfer-encoding".into());
assert!(err.to_string().contains("transfer-encoding"));
let err = ResponseConstructionError::BodyAlreadyConsumed;
assert!(!err.to_string().is_empty());
let err = ResponseConstructionError::ContentLengthMismatch {
declared: 100,
actual: 50,
};
assert!(err.to_string().contains("100"));
assert!(err.to_string().contains("50"));
}
#[test]
fn status_code_display() {
assert_eq!(format!("{}", StatusCode::OK), "200");
assert_eq!(format!("{}", StatusCode::NOT_FOUND), "404");
}
#[test]
fn status_code_into_u16() {
let code: u16 = StatusCode::OK.into();
assert_eq!(code, 200);
}
#[test]
fn is_hop_by_hop_header_recognizes_all_variants() {
assert!(is_hop_by_hop_header("connection"));
assert!(is_hop_by_hop_header("Connection"));
assert!(is_hop_by_hop_header("CONNECTION"));
assert!(is_hop_by_hop_header("keep-alive"));
assert!(is_hop_by_hop_header("Keep-Alive"));
assert!(is_hop_by_hop_header("proxy-authenticate"));
assert!(is_hop_by_hop_header("proxy-authorization"));
assert!(is_hop_by_hop_header("proxy-connection"));
assert!(is_hop_by_hop_header("te"));
assert!(is_hop_by_hop_header("TE"));
assert!(is_hop_by_hop_header("trailer"));
assert!(is_hop_by_hop_header("Trailer"));
assert!(is_hop_by_hop_header("transfer-encoding"));
assert!(is_hop_by_hop_header("Transfer-Encoding"));
assert!(is_hop_by_hop_header("upgrade"));
assert!(is_hop_by_hop_header("Upgrade"));
}
#[test]
fn is_hop_by_hop_header_rejects_end_to_end() {
assert!(!is_hop_by_hop_header("content-type"));
assert!(!is_hop_by_hop_header("content-length"));
assert!(!is_hop_by_hop_header("host"));
assert!(!is_hop_by_hop_header("set-cookie"));
assert!(!is_hop_by_hop_header("etag"));
assert!(!is_hop_by_hop_header("authorization"));
assert!(!is_hop_by_hop_header("cache-control"));
}
#[test]
fn normalize_metadata_strips_all_hop_by_hop() {
let code = StatusCode::OK;
let mut headers = HeaderBlock::new();
headers.push_str("content-type", "text/plain").unwrap();
headers.push_str("transfer-encoding", "chunked").unwrap();
headers.push_str("connection", "keep-alive").unwrap();
headers.push_str("trailer", "x-checksum").unwrap();
headers.push_str("upgrade", "h2c").unwrap();
headers.push_str("te", "deflate").unwrap();
normalize_metadata(code, &mut headers, 5, false).unwrap();
assert!(!headers.contains("transfer-encoding"));
assert!(!headers.contains("connection"));
assert!(!headers.contains("trailer"));
assert!(!headers.contains("upgrade"));
assert!(!headers.contains("te"));
assert!(headers.contains("content-type"));
assert_eq!(headers.get_first("content-length").unwrap().as_str(), "5");
}
#[test]
fn duplicate_content_length_replaced_by_normalized_value() {
let code = StatusCode::OK;
let mut headers = HeaderBlock::new();
headers.push_str("content-length", "999").unwrap();
headers.push_str("content-length", "888").unwrap();
normalize_metadata(code, &mut headers, 42, false).unwrap();
let all_cl = headers.get_all("content-length");
assert_eq!(all_cl.len(), 1, "only one Content-Length must remain");
assert_eq!(all_cl[0].as_str(), "42");
}
#[test]
fn transfer_encoding_plus_content_length_strips_te() {
let resp = Response::builder()
.status(StatusCode::OK)
.header("transfer-encoding", "chunked")
.unwrap()
.header("content-length", "100")
.unwrap()
.body(ResponseBody::Bytes(b"hello".to_vec()))
.unwrap();
let req = NormalizeRequest::new(false);
let normalized = normalize_response(resp, &req).unwrap();
assert!(!normalized.headers().contains("transfer-encoding"));
assert_eq!(
normalized
.headers()
.get_first("content-length")
.unwrap()
.as_str(),
"5"
);
}
#[test]
fn normalize_metadata_preserves_duplicate_set_cookie() {
let code = StatusCode::OK;
let mut headers = HeaderBlock::new();
headers.push_str("set-cookie", "a=1").unwrap();
headers.push_str("set-cookie", "b=2").unwrap();
normalize_metadata(code, &mut headers, 0, false).unwrap();
let all = headers.get_all("set-cookie");
assert_eq!(all.len(), 2);
assert_eq!(all[0].as_str(), "a=1");
assert_eq!(all[1].as_str(), "b=2");
}
#[test]
fn normalize_metadata_head_preserves_content_length_when_body_nonempty() {
let code = StatusCode::OK;
let mut headers = HeaderBlock::new();
headers.push_str("content-length", "100").unwrap();
normalize_metadata(code, &mut headers, 100, true).unwrap();
assert_eq!(
headers.get_first("content-length").unwrap().as_str(),
"100",
"HEAD with non-empty body must preserve Content-Length"
);
}
#[test]
fn normalize_metadata_head_suppresses_content_length_when_body_empty() {
let code = StatusCode::OK;
let mut headers = HeaderBlock::new();
headers.push_str("content-length", "100").unwrap();
normalize_metadata(code, &mut headers, 0, true).unwrap();
assert!(
!headers.contains("content-length"),
"HEAD with empty body must suppress Content-Length"
);
}
}