use crate::Version;
use crate::header::{HeaderId, HeaderVec};
use bytes::{BufMut, Bytes, BytesMut};
use std::time::SystemTime;
const DATE_LEN: usize = 29;
#[derive(Debug)]
pub struct DateCache {
secs: u64,
buf: [u8; DATE_LEN],
valid: bool,
}
impl Default for DateCache {
fn default() -> Self {
Self::new()
}
}
impl DateCache {
pub fn new() -> Self {
Self {
secs: 0,
buf: [0; DATE_LEN],
valid: false,
}
}
pub fn get(&mut self, now: SystemTime) -> &[u8] {
let secs = now
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if !self.valid || secs != self.secs {
let formatted = httpdate::fmt_http_date(now);
debug_assert_eq!(formatted.len(), DATE_LEN);
let bytes = formatted.as_bytes();
let n = bytes.len().min(DATE_LEN);
self.buf[..n].copy_from_slice(&bytes[..n]);
self.secs = secs;
self.valid = true;
}
&self.buf
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OutBody {
None,
Fixed(Bytes),
Chunked,
}
#[derive(Clone, Debug, Default)]
pub struct ResponseHead {
pub status: u16,
pub headers: HeaderVec,
}
pub fn reason_phrase(status: u16) -> &'static str {
match status {
100 => "Continue",
101 => "Switching Protocols",
200 => "OK",
201 => "Created",
202 => "Accepted",
204 => "No Content",
206 => "Partial Content",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
408 => "Request Timeout",
409 => "Conflict",
411 => "Length Required",
412 => "Precondition Failed",
413 => "Content Too Large",
414 => "URI Too Long",
415 => "Unsupported Media Type",
416 => "Range Not Satisfiable",
417 => "Expectation Failed",
421 => "Misdirected Request",
422 => "Unprocessable Content",
426 => "Upgrade Required",
428 => "Precondition Required",
429 => "Too Many Requests",
431 => "Request Header Fields Too Large",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
505 => "HTTP Version Not Supported",
_ => "",
}
}
pub fn write_u64(out: &mut BytesMut, v: u64) {
let mut scratch = [0u8; 20];
let mut i = scratch.len();
let mut n = v;
loop {
i -= 1;
scratch[i] = b'0' + (n % 10) as u8;
n /= 10;
if n == 0 {
break;
}
}
out.put_slice(&scratch[i..]);
}
#[inline]
pub(crate) fn forbids_body_framing(status: u16) -> bool {
matches!(status, 204 | 304) || (100..200).contains(&status)
}
#[inline]
pub(crate) fn content_length_is_descriptive(status: u16) -> bool {
status == 304
}
#[inline]
pub(crate) fn connection_field(version: Version, keep_alive: bool) -> Option<&'static str> {
match (version, keep_alive) {
(Version::Http11, true) => None,
(Version::Http10, true) => Some("keep-alive"),
(_, false) => Some("close"),
}
}
#[inline]
fn valid_field_value(value: &[u8]) -> bool {
!value.iter().any(|b| matches!(b, b'\r' | b'\n' | 0))
}
#[inline]
fn valid_field_name(name: &str) -> bool {
!name.is_empty()
&& name.bytes().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
}
#[inline]
fn writable(id: &HeaderId, value: &[u8]) -> bool {
let name_ok = match id {
HeaderId::Other(name) => valid_field_name(name.as_str()),
_ => true,
};
name_ok && valid_field_value(value)
}
#[inline]
pub(crate) fn parse_len(v: &Bytes) -> Option<u64> {
if v.is_empty() || !v.iter().all(u8::is_ascii_digit) {
return None;
}
std::str::from_utf8(v).ok()?.parse().ok()
}
#[inline]
fn te_frames_chunked(v: &Bytes) -> bool {
let Ok(s) = std::str::from_utf8(v) else {
return false;
};
s.split(',')
.map(str::trim)
.rfind(|c| !c.is_empty())
.is_some_and(|c| c.eq_ignore_ascii_case("chunked"))
}
pub fn write_head(
out: &mut BytesMut,
version: Version,
resp: &ResponseHead,
body: &OutBody,
date: &[u8],
keep_alive: bool,
) {
out.put_slice(version.as_bytes());
out.put_u8(b' ');
write_u64(out, resp.status as u64);
let phrase = reason_phrase(resp.status);
if !phrase.is_empty() {
out.put_u8(b' ');
out.put_slice(phrase.as_bytes());
}
out.put_slice(b"\r\n");
let mut field_writable: smallvec::SmallVec<[bool; 16]> =
smallvec::SmallVec::with_capacity(resp.headers.len());
let mut has_date = false;
let mut has_connection = false;
let mut handler_len: Option<&Bytes> = None;
let mut handler_te: Option<&Bytes> = None;
let mut len_count = 0usize;
let mut te_count = 0usize;
for (id, value) in resp.headers.iter() {
let ok = writable(id, value);
field_writable.push(ok);
if !ok {
continue;
}
match id {
HeaderId::Date => has_date = true,
HeaderId::Connection => has_connection = true,
HeaderId::ContentLength => {
len_count += 1;
if handler_len.is_none() {
handler_len = Some(value);
}
}
HeaderId::TransferEncoding => {
te_count += 1;
if handler_te.is_none() {
handler_te = Some(value);
}
}
_ => {}
}
}
let stale_content_length = handler_len.is_some_and(|v| {
len_count > 1
|| if content_length_is_descriptive(resp.status) {
parse_len(v).is_none()
} else {
match body {
OutBody::Fixed(b) => parse_len(v) != Some(b.len() as u64),
OutBody::None => parse_len(v) != Some(0),
OutBody::Chunked => true,
}
}
});
let stale_transfer_encoding = handler_te
.is_some_and(|v| te_count > 1 || *body != OutBody::Chunked || !te_frames_chunked(v));
for (i, (id, value)) in resp.headers.iter().enumerate() {
if stale_content_length && id == &HeaderId::ContentLength {
tracing::warn!("dropping response content-length that disagrees with the body length");
continue;
}
if stale_transfer_encoding && id == &HeaderId::TransferEncoding {
tracing::warn!("dropping response transfer-encoding that does not frame the body");
continue;
}
if !field_writable[i] {
tracing::warn!(
field = id.as_str(),
"dropping response header with an invalid field name or value"
);
continue;
}
out.put_slice(id.as_str().as_bytes());
out.put_slice(b": ");
out.put_slice(value);
out.put_slice(b"\r\n");
}
if !has_date {
out.put_slice(b"date: ");
out.put_slice(date);
out.put_slice(b"\r\n");
}
let handler_framed = (!stale_content_length && handler_len.is_some())
|| (!stale_transfer_encoding && handler_te.is_some());
if !handler_framed && !forbids_body_framing(resp.status) {
match body {
OutBody::Fixed(b) => {
out.put_slice(b"content-length: ");
write_u64(out, b.len() as u64);
out.put_slice(b"\r\n");
}
OutBody::Chunked => {
out.put_slice(b"transfer-encoding: chunked\r\n");
}
OutBody::None => {
out.put_slice(b"content-length: 0\r\n");
}
}
}
if !has_connection && let Some(token) = connection_field(version, keep_alive) {
out.put_slice(b"connection: ");
out.put_slice(token.as_bytes());
out.put_slice(b"\r\n");
}
out.put_slice(b"\r\n");
}
pub fn write_chunk(out: &mut BytesMut, data: &[u8]) {
write_hex(out, data.len() as u64);
out.put_slice(b"\r\n");
out.put_slice(data);
out.put_slice(b"\r\n");
}
pub fn write_last_chunk(out: &mut BytesMut, trailers: &HeaderVec) {
out.put_slice(b"0\r\n");
for (id, value) in trailers.iter() {
if !writable(id, value) {
tracing::warn!(
field = id.as_str(),
"dropping trailer with an invalid field name or value"
);
continue;
}
out.put_slice(id.as_str().as_bytes());
out.put_slice(b": ");
out.put_slice(value);
out.put_slice(b"\r\n");
}
out.put_slice(b"\r\n");
}
fn write_hex(out: &mut BytesMut, v: u64) {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut scratch = [0u8; 16];
let mut i = scratch.len();
let mut n = v;
loop {
i -= 1;
scratch[i] = HEX[(n % 16) as usize];
n /= 16;
if n == 0 {
break;
}
}
out.put_slice(&scratch[i..]);
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
const EPOCH_DATE: &[u8] = b"Thu, 01 Jan 1970 00:00:00 GMT";
fn head_of(status: u16, headers: &[(HeaderId, &'static str)]) -> ResponseHead {
ResponseHead {
status,
headers: headers
.iter()
.map(|(id, v)| (id.clone(), Bytes::from_static(v.as_bytes())))
.collect(),
}
}
fn render(version: Version, resp: &ResponseHead, body: &OutBody, keep_alive: bool) -> String {
let mut out = BytesMut::new();
write_head(&mut out, version, resp, body, EPOCH_DATE, keep_alive);
String::from_utf8(out.to_vec()).unwrap()
}
#[test]
fn writes_a_minimal_200() {
let got = render(
Version::Http11,
&head_of(200, &[]),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert_eq!(
got,
"HTTP/1.1 200 OK\r\n\
date: Thu, 01 Jan 1970 00:00:00 GMT\r\n\
content-length: 5\r\n\
\r\n"
);
}
#[test]
fn write_u64_matches_to_string() {
for v in [0u64, 1, 9, 10, 99, 100, 12345, u64::MAX] {
let mut out = BytesMut::new();
write_u64(&mut out, v);
assert_eq!(String::from_utf8(out.to_vec()).unwrap(), v.to_string());
}
}
#[test]
fn reason_phrases_are_correct() {
assert_eq!(reason_phrase(200), "OK");
assert_eq!(reason_phrase(201), "Created");
assert_eq!(reason_phrase(204), "No Content");
assert_eq!(reason_phrase(301), "Moved Permanently");
assert_eq!(reason_phrase(304), "Not Modified");
assert_eq!(reason_phrase(400), "Bad Request");
assert_eq!(reason_phrase(404), "Not Found");
assert_eq!(reason_phrase(408), "Request Timeout");
assert_eq!(reason_phrase(413), "Content Too Large");
assert_eq!(reason_phrase(431), "Request Header Fields Too Large");
assert_eq!(reason_phrase(500), "Internal Server Error");
assert_eq!(reason_phrase(501), "Not Implemented");
assert_eq!(reason_phrase(505), "HTTP Version Not Supported");
assert_eq!(reason_phrase(599), "", "unregistered codes invent nothing");
}
#[test]
fn fixed_body_emits_content_length() {
let got = render(
Version::Http11,
&head_of(200, &[]),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert!(got.contains("content-length: 5\r\n"));
assert!(!got.contains("transfer-encoding"));
}
#[test]
fn chunked_body_emits_transfer_encoding() {
let got = render(Version::Http11, &head_of(200, &[]), &OutBody::Chunked, true);
assert!(got.contains("transfer-encoding: chunked\r\n"));
assert!(!got.contains("content-length"));
}
#[test]
fn empty_body_emits_zero_length() {
let got = render(Version::Http11, &head_of(200, &[]), &OutBody::None, true);
assert!(got.contains("content-length: 0\r\n"));
}
#[test]
fn no_framing_fields_on_204_or_304() {
for status in [204u16, 304] {
let got = render(Version::Http11, &head_of(status, &[]), &OutBody::None, true);
assert!(
!got.contains("content-length"),
"{status} must not frame a body: {got}"
);
assert!(
!got.contains("transfer-encoding"),
"{status} must not frame a body: {got}"
);
}
}
#[test]
fn does_not_duplicate_handler_supplied_date() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::Date, "Mon, 01 Jan 2001 00:00:00 GMT")]),
&OutBody::None,
true,
);
assert_eq!(got.matches("date: ").count(), 1);
assert!(got.contains("Mon, 01 Jan 2001"));
}
#[test]
fn does_not_duplicate_handler_supplied_content_length() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::ContentLength, "5")]),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert_eq!(got.matches("content-length").count(), 1);
}
#[test]
fn does_not_add_content_length_when_handler_set_transfer_encoding() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::TransferEncoding, "chunked")]),
&OutBody::Chunked,
true,
);
assert_eq!(got.matches("transfer-encoding").count(), 1);
assert!(!got.contains("content-length"));
}
#[test]
fn a_transfer_encoding_over_a_fixed_body_is_dropped() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::TransferEncoding, "chunked")]),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert!(!got.contains("transfer-encoding"), "{got}");
assert_eq!(got.matches("content-length").count(), 1, "{got}");
assert!(got.contains("content-length: 5\r\n"), "{got}");
}
#[test]
fn a_transfer_encoding_over_no_body_is_dropped() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::TransferEncoding, "chunked")]),
&OutBody::None,
true,
);
assert!(!got.contains("transfer-encoding"), "{got}");
assert!(got.contains("content-length: 0\r\n"), "{got}");
}
#[test]
fn a_non_chunked_transfer_encoding_over_a_chunked_body_is_replaced() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::TransferEncoding, "gzip")]),
&OutBody::Chunked,
true,
);
assert!(!got.contains("gzip"), "{got}");
assert_eq!(got.matches("transfer-encoding").count(), 1, "{got}");
assert!(got.contains("transfer-encoding: chunked\r\n"), "{got}");
}
#[test]
fn a_transfer_encoding_ending_in_chunked_is_preserved() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::TransferEncoding, "gzip, chunked")]),
&OutBody::Chunked,
true,
);
assert_eq!(got.matches("transfer-encoding").count(), 1, "{got}");
assert!(
got.contains("transfer-encoding: gzip, chunked\r\n"),
"{got}"
);
assert!(!got.contains("content-length"), "{got}");
}
#[test]
fn a_handler_content_length_survives_on_304_but_not_204() {
let got = render(
Version::Http11,
&head_of(304, &[(HeaderId::ContentLength, "1234")]),
&OutBody::None,
true,
);
assert!(got.contains("content-length: 1234\r\n"), "{got}");
assert_eq!(got.matches("content-length").count(), 1, "{got}");
let got = render(
Version::Http11,
&head_of(204, &[(HeaderId::ContentLength, "1234")]),
&OutBody::None,
true,
);
assert!(!got.contains("content-length"), "{got}");
}
#[test]
fn a_malformed_content_length_is_dropped_on_304() {
let got = render(
Version::Http11,
&head_of(304, &[(HeaderId::ContentLength, "abc")]),
&OutBody::None,
true,
);
assert!(!got.contains("content-length"), "{got}");
let got = render(
Version::Http11,
&head_of(304, &[(HeaderId::ContentLength, "9")]),
&OutBody::None,
true,
);
assert!(got.contains("content-length: 9\r\n"), "{got}");
}
#[test]
fn duplicate_transfer_encoding_is_stale_and_reclaimed() {
let got = render(
Version::Http11,
&head_of(
200,
&[
(HeaderId::TransferEncoding, "chunked"),
(HeaderId::TransferEncoding, "gzip"),
],
),
&OutBody::Chunked,
true,
);
assert_eq!(got.matches("transfer-encoding").count(), 1, "{got}");
assert!(got.contains("transfer-encoding: chunked\r\n"), "{got}");
assert!(!got.contains("gzip"), "{got}");
}
#[test]
fn duplicate_transfer_encoding_is_stale_even_when_identical() {
let got = render(
Version::Http11,
&head_of(
200,
&[
(HeaderId::TransferEncoding, "chunked"),
(HeaderId::TransferEncoding, "chunked"),
],
),
&OutBody::Chunked,
true,
);
assert_eq!(got.matches("transfer-encoding").count(), 1, "{got}");
assert!(got.contains("transfer-encoding: chunked\r\n"), "{got}");
}
#[test]
fn duplicate_content_length_is_stale_and_reclaimed() {
let got = render(
Version::Http11,
&head_of(
200,
&[
(HeaderId::ContentLength, "5"),
(HeaderId::ContentLength, "99"),
],
),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert_eq!(got.matches("content-length").count(), 1, "{got}");
assert!(got.contains("content-length: 5\r\n"), "{got}");
assert!(!got.contains("99"), "{got}");
let got = render(
Version::Http11,
&head_of(
200,
&[
(HeaderId::ContentLength, "5"),
(HeaderId::ContentLength, "5"),
],
),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert_eq!(got.matches("content-length").count(), 1, "{got}");
assert!(got.contains("content-length: 5\r\n"), "{got}");
}
#[test]
fn duplicate_content_length_is_stale_on_304() {
let got = render(
Version::Http11,
&head_of(
304,
&[
(HeaderId::ContentLength, "1234"),
(HeaderId::ContentLength, "1234"),
],
),
&OutBody::None,
true,
);
assert!(!got.contains("content-length"), "{got}");
}
#[test]
fn an_unwritable_duplicate_does_not_make_the_first_stale() {
let got = render(
Version::Http11,
&head_of(
200,
&[
(HeaderId::ContentLength, "5"),
(HeaderId::ContentLength, "5\r\nx: y"),
],
),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert_eq!(got.matches("content-length").count(), 1, "{got}");
assert!(got.contains("content-length: 5\r\n"), "{got}");
assert!(!got.contains("x: y"), "{got}");
}
#[test]
fn connection_field_covers_every_arm() {
assert_eq!(connection_field(Version::Http11, true), None);
assert_eq!(connection_field(Version::Http10, true), Some("keep-alive"));
assert_eq!(connection_field(Version::Http11, false), Some("close"));
assert_eq!(connection_field(Version::Http10, false), Some("close"));
}
#[test]
fn content_length_is_descriptive_only_on_304() {
assert!(content_length_is_descriptive(304));
for status in [100u16, 200, 204, 400, 500] {
assert!(!content_length_is_descriptive(status), "{status}");
}
}
#[test]
fn emits_connection_close_when_not_keep_alive() {
let got = render(Version::Http11, &head_of(200, &[]), &OutBody::None, false);
assert!(got.contains("connection: close\r\n"));
}
#[test]
fn omits_connection_header_when_keep_alive_on_http11() {
let got = render(Version::Http11, &head_of(200, &[]), &OutBody::None, true);
assert!(
!got.contains("connection:"),
"persistence is the HTTP/1.1 default; stating it is noise"
);
}
#[test]
fn emits_connection_keep_alive_on_http10() {
let got = render(Version::Http10, &head_of(200, &[]), &OutBody::None, true);
assert!(got.contains("connection: keep-alive\r\n"));
assert!(got.starts_with("HTTP/1.0 200 OK\r\n"));
}
#[test]
fn does_not_duplicate_handler_supplied_connection() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::Connection, "close")]),
&OutBody::None,
false,
);
assert_eq!(got.matches("connection").count(), 1);
}
#[test]
fn drops_header_values_that_would_split_the_response() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::Location, "/a\r\nX-Injected: 1")]),
&OutBody::None,
true,
);
assert!(!got.contains("X-Injected"), "{got}");
assert!(
!got.contains("location"),
"the whole field goes, not just the tail: {got}"
);
}
#[test]
fn drops_values_containing_bare_cr_lf_or_nul() {
for bad in ["a\rb", "a\nb", "a\0b"] {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::Etag, bad)]),
&OutBody::None,
true,
);
assert!(!got.contains("etag"), "{bad:?} must be dropped: {got}");
}
}
#[test]
fn drops_custom_field_names_that_are_not_tokens() {
let mut headers = HeaderVec::new();
for name in ["x bad", "x:bad", "x\r\nbad", ""] {
headers.push((
HeaderId::Other(crate::ByteStr::from(name)),
Bytes::from_static(b"1"),
));
}
headers.push((
HeaderId::Other(crate::ByteStr::from_static("x-good")),
Bytes::from_static(b"1"),
));
let got = render(
Version::Http11,
&ResponseHead {
status: 200,
headers,
},
&OutBody::None,
true,
);
assert!(!got.contains("bad"), "{got}");
assert!(got.contains("x-good: 1\r\n"), "{got}");
}
#[test]
fn a_dropped_content_length_does_not_suppress_framing() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::ContentLength, "5\r\nX-Injected: 1")]),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert!(!got.contains("X-Injected"), "{got}");
assert_eq!(got.matches("content-length").count(), 1, "{got}");
assert!(got.contains("content-length: 5\r\n"), "{got}");
}
#[test]
fn write_last_chunk_drops_an_injecting_trailer() {
let mut trailers = HeaderVec::new();
trailers.push((HeaderId::Etag, Bytes::from_static(b"x\r\nX-Injected: 1")));
let mut out = BytesMut::new();
write_last_chunk(&mut out, &trailers);
assert_eq!(&out[..], b"0\r\n\r\n");
}
#[test]
fn date_cache_reformats_only_on_second_change() {
let mut c = DateCache::new();
let t0 = SystemTime::UNIX_EPOCH + Duration::from_millis(1_500);
let first = c.get(t0).to_vec();
let same_second = c.get(t0 + Duration::from_millis(400)).to_vec();
assert_eq!(first, same_second);
let next_second = c.get(t0 + Duration::from_secs(1)).to_vec();
assert_ne!(first, next_second);
}
#[test]
fn date_format_is_imf_fixdate() {
let mut c = DateCache::new();
let d = c.get(SystemTime::UNIX_EPOCH);
assert_eq!(d.len(), DATE_LEN);
assert_eq!(d, EPOCH_DATE);
}
#[test]
fn write_chunk_frames_correctly() {
let mut out = BytesMut::new();
write_chunk(&mut out, b"hello");
assert_eq!(&out[..], b"5\r\nhello\r\n");
let mut out = BytesMut::new();
write_chunk(&mut out, &[0u8; 31]);
assert!(out.starts_with(b"1f\r\n"), "sizes are lowercase hex");
}
#[test]
fn write_last_chunk_without_trailers() {
let mut out = BytesMut::new();
write_last_chunk(&mut out, &HeaderVec::new());
assert_eq!(&out[..], b"0\r\n\r\n");
}
#[test]
fn write_last_chunk_with_trailers() {
let mut trailers = HeaderVec::new();
trailers.push((HeaderId::Etag, Bytes::from_static(b"x")));
let mut out = BytesMut::new();
write_last_chunk(&mut out, &trailers);
assert_eq!(&out[..], b"0\r\netag: x\r\n\r\n");
}
#[test]
fn written_head_parses_as_a_valid_message() {
let got = render(
Version::Http11,
&head_of(200, &[(HeaderId::ContentType, "text/plain")]),
&OutBody::Fixed(Bytes::from_static(b"hello")),
true,
);
assert!(crate::parse::prescan(got.as_bytes()).is_ok());
assert_eq!(crate::parse::find_head_end(got.as_bytes()), Some(got.len()));
}
#[test]
fn a_wrong_handler_content_length_is_replaced_with_the_true_one() {
let mut headers = HeaderVec::new();
headers.push((HeaderId::ContentLength, Bytes::from_static(b"5")));
let head = ResponseHead {
status: 200,
headers,
};
let mut out = BytesMut::new();
let body = OutBody::Fixed(Bytes::from_static(b"hello world"));
write_head(&mut out, Version::Http11, &head, &body, b"D", true);
let text = String::from_utf8_lossy(&out).to_lowercase();
assert_eq!(text.matches("content-length:").count(), 1, "{text}");
assert!(text.contains("content-length: 11"), "{text}");
assert!(!text.contains("content-length: 5"), "{text}");
}
#[test]
fn a_correct_handler_content_length_is_preserved() {
let mut headers = HeaderVec::new();
headers.push((HeaderId::ContentLength, Bytes::from_static(b"11")));
let head = ResponseHead {
status: 200,
headers,
};
let mut out = BytesMut::new();
let body = OutBody::Fixed(Bytes::from_static(b"hello world"));
write_head(&mut out, Version::Http11, &head, &body, b"D", true);
let text = String::from_utf8_lossy(&out).to_lowercase();
assert_eq!(text.matches("content-length:").count(), 1, "{text}");
assert!(text.contains("content-length: 11"), "{text}");
}
}