1use std::fmt;
18
19use super::body::BodySource;
20use super::header_block::{HeaderBlock, HeaderError, HeaderName, HeaderValue};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ResponseConstructionError {
25 InvalidStatus(u16),
27 InvalidHeader(HeaderError),
29 ForbiddenFramingHeader(String),
32 BodyAlreadyConsumed,
34 ContentLengthMismatch { declared: u64, actual: u64 },
36 FileStreamLimit,
38}
39
40impl fmt::Display for ResponseConstructionError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::InvalidStatus(code) => write!(f, "invalid status code: {}", code),
44 Self::InvalidHeader(e) => write!(f, "invalid header: {}", e),
45 Self::ForbiddenFramingHeader(name) => {
46 write!(f, "forbidden framing header: {}", name)
47 }
48 Self::BodyAlreadyConsumed => write!(f, "response body already consumed"),
49 Self::ContentLengthMismatch { declared, actual } => {
50 write!(
51 f,
52 "content-length mismatch: declared {}, actual {}",
53 declared, actual
54 )
55 }
56 Self::FileStreamLimit => write!(f, "file stream admission limit reached"),
57 }
58 }
59}
60
61impl std::error::Error for ResponseConstructionError {}
62
63impl From<HeaderError> for ResponseConstructionError {
64 fn from(e: HeaderError) -> Self {
65 Self::InvalidHeader(e)
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub struct StatusCode(u16);
75
76impl StatusCode {
77 pub const CONTINUE: Self = Self(100);
78 pub const SWITCHING_PROTOCOLS: Self = Self(101);
79 pub const OK: Self = Self(200);
80 pub const CREATED: Self = Self(201);
81 pub const NO_CONTENT: Self = Self(204);
82 pub const RESET_CONTENT: Self = Self(205);
83 pub const NOT_MODIFIED: Self = Self(304);
84 pub const MOVED_PERMANENTLY: Self = Self(301);
85 pub const BAD_REQUEST: Self = Self(400);
86 pub const FORBIDDEN: Self = Self(403);
87 pub const NOT_FOUND: Self = Self(404);
88 pub const METHOD_NOT_ALLOWED: Self = Self(405);
89 pub const REQUEST_TIMEOUT: Self = Self(408);
90 pub const PAYLOAD_TOO_LARGE: Self = Self(413);
91 pub const RANGE_NOT_SATISFIABLE: Self = Self(416);
92 pub const INTERNAL_SERVER_ERROR: Self = Self(500);
93 pub const SERVICE_UNAVAILABLE: Self = Self(503);
94
95 pub fn new(code: u16) -> Result<Self, ResponseConstructionError> {
102 if !(100..=599).contains(&code) {
103 return Err(ResponseConstructionError::InvalidStatus(code));
104 }
105 Ok(Self(code))
106 }
107
108 pub fn as_u16(&self) -> u16 {
110 self.0
111 }
112
113 pub fn is_informational(&self) -> bool {
115 (100..200).contains(&self.0)
116 }
117
118 pub fn is_success(&self) -> bool {
120 (200..300).contains(&self.0)
121 }
122
123 pub fn is_redirection(&self) -> bool {
125 (300..400).contains(&self.0)
126 }
127
128 pub fn is_client_error(&self) -> bool {
130 (400..500).contains(&self.0)
131 }
132
133 pub fn is_server_error(&self) -> bool {
135 (500..600).contains(&self.0)
136 }
137
138 pub fn permits_payload_body(&self) -> bool {
143 !self.is_informational() && self.0 != 204 && self.0 != 205 && self.0 != 304
144 }
145}
146
147impl fmt::Display for StatusCode {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{}", self.0)
150 }
151}
152
153impl From<StatusCode> for u16 {
154 fn from(s: StatusCode) -> u16 {
155 s.0
156 }
157}
158
159#[derive(Debug, Clone)]
164pub struct ResponseHead {
165 status: StatusCode,
166 headers: HeaderBlock,
167}
168
169impl ResponseHead {
170 pub fn new(status: StatusCode, headers: HeaderBlock) -> Self {
172 Self { status, headers }
173 }
174
175 pub fn status(&self) -> StatusCode {
177 self.status
178 }
179
180 pub fn headers(&self) -> &HeaderBlock {
182 &self.headers
183 }
184
185 pub fn headers_mut(&mut self) -> &mut HeaderBlock {
190 &mut self.headers
191 }
192}
193
194#[derive(Debug)]
199pub enum ResponseBody {
200 Empty,
202 Bytes(Vec<u8>),
204 File(BodySource),
207 EmptyWithLength(u64),
210}
211
212impl ResponseBody {
213 pub fn len(&self) -> u64 {
215 match self {
216 Self::Empty => 0,
217 Self::Bytes(b) => b.len() as u64,
218 Self::File(source) => source.len(),
219 Self::EmptyWithLength(len) => *len,
220 }
221 }
222
223 pub fn is_empty(&self) -> bool {
225 self.len() == 0
226 }
227
228 pub fn into_bytes(self) -> Option<Vec<u8>> {
232 match self {
233 Self::Empty => None,
234 Self::Bytes(b) => Some(b),
235 Self::File(_) => None,
236 Self::EmptyWithLength(_) => None,
237 }
238 }
239}
240
241pub struct Response {
252 head: ResponseHead,
253 body: Option<ResponseBody>,
254}
255
256impl Response {
257 pub fn builder() -> ResponseBuilder {
259 ResponseBuilder {
260 status: None,
261 headers: HeaderBlock::new(),
262 }
263 }
264
265 pub fn head(&self) -> &ResponseHead {
267 &self.head
268 }
269
270 pub fn head_mut(&mut self) -> &mut ResponseHead {
272 &mut self.head
273 }
274
275 pub fn status(&self) -> StatusCode {
277 self.head.status()
278 }
279
280 pub fn headers(&self) -> &HeaderBlock {
282 self.head.headers()
283 }
284
285 pub fn take_body(&mut self) -> Option<ResponseBody> {
289 self.body.take()
290 }
291
292 pub fn body(&self) -> Option<&ResponseBody> {
294 self.body.as_ref()
295 }
296}
297
298impl fmt::Debug for Response {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.debug_struct("Response")
301 .field("head", &self.head)
302 .field("body", &self.body)
303 .finish()
304 }
305}
306
307pub struct ResponseBuilder {
323 status: Option<StatusCode>,
324 headers: HeaderBlock,
325}
326
327impl ResponseBuilder {
328 pub fn status(mut self, status: StatusCode) -> Self {
330 self.status = Some(status);
331 self
332 }
333
334 pub fn push_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
340 self.headers.push(name, value);
341 self
342 }
343
344 pub fn header(
351 mut self,
352 name: impl Into<String>,
353 value: impl Into<String>,
354 ) -> Result<Self, ResponseConstructionError> {
355 let name = HeaderName::new(name)?;
356 let value = HeaderValue::new(value)?;
357 self.headers.push(name, value);
358 Ok(self)
359 }
360
361 pub fn body(self, body: ResponseBody) -> Result<Response, ResponseConstructionError> {
367 let status = self
368 .status
369 .ok_or(ResponseConstructionError::InvalidStatus(0))?;
370 Ok(Response {
371 head: ResponseHead::new(status, self.headers),
372 body: Some(body),
373 })
374 }
375
376 pub fn empty(self) -> Result<Response, ResponseConstructionError> {
378 self.body(ResponseBody::Empty)
379 }
380}
381
382pub struct NormalizeRequest {
384 pub is_head: bool,
386}
387
388impl NormalizeRequest {
389 pub fn new(is_head: bool) -> Self {
391 Self { is_head }
392 }
393}
394
395pub fn normalize_response(
415 mut response: Response,
416 request: &NormalizeRequest,
417) -> Result<Response, ResponseConstructionError> {
418 let status = response.status();
419
420 let body_len = response.body.as_ref().map_or(0, |b| b.len());
422
423 if request.is_head {
425 response.body = Some(ResponseBody::Empty);
426 }
427
428 if !status.permits_payload_body() {
430 response.body = Some(ResponseBody::Empty);
431 }
432
433 normalize_metadata(
435 status,
436 response.head.headers_mut(),
437 body_len,
438 request.is_head,
439 )?;
440
441 Ok(response)
442}
443
444pub fn normalize_metadata(
478 status: StatusCode,
479 headers: &mut HeaderBlock,
480 body_len: u64,
481 is_head: bool,
482) -> Result<(), ResponseConstructionError> {
483 strip_hop_by_hop(headers);
485
486 let not_modified_length = if status == StatusCode::NOT_MODIFIED {
489 headers
490 .get_unique("content-length")
491 .ok()
492 .flatten()
493 .and_then(|value| value.as_str().parse::<u64>().ok())
494 .filter(|length| *length == body_len)
495 } else {
496 None
497 };
498
499 remove_header(headers, "content-length");
501
502 if (status.permits_payload_body() && !(is_head && body_len == 0))
503 || not_modified_length.is_some()
504 {
505 let length = not_modified_length.unwrap_or(body_len);
506 headers
507 .push_str("content-length", length.to_string())
508 .map_err(ResponseConstructionError::from)?;
509 }
510
511 Ok(())
512}
513
514pub fn is_hop_by_hop_header(name: &str) -> bool {
517 matches!(
518 name.to_ascii_lowercase().as_str(),
519 "connection"
520 | "keep-alive"
521 | "proxy-authenticate"
522 | "proxy-authorization"
523 | "proxy-connection"
524 | "te"
525 | "trailer"
526 | "transfer-encoding"
527 | "upgrade"
528 )
529}
530
531fn remove_header(headers: &mut HeaderBlock, name: &str) {
533 let lower = name.to_ascii_lowercase();
534 headers.retain(|f| f.name.as_str().to_ascii_lowercase() != lower);
535}
536
537fn strip_hop_by_hop(headers: &mut HeaderBlock) {
539 headers.retain(|f| !is_hop_by_hop_header(f.name.as_str()));
540}
541
542pub fn to_hyper_response(
546 response: Response,
547) -> Result<
548 hyper::Response<http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error>>,
549 ResponseConstructionError,
550> {
551 to_hyper_response_with_optional_file_stream_semaphore(response, None)
552}
553
554pub fn to_hyper_response_with_file_stream_semaphore(
557 response: Response,
558 semaphore: &std::sync::Arc<tokio::sync::Semaphore>,
559) -> Result<
560 hyper::Response<http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error>>,
561 ResponseConstructionError,
562> {
563 to_hyper_response_with_optional_file_stream_semaphore(response, Some(semaphore))
564}
565
566fn to_hyper_response_with_optional_file_stream_semaphore(
567 response: Response,
568 semaphore: Option<&std::sync::Arc<tokio::sync::Semaphore>>,
569) -> Result<
570 hyper::Response<http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error>>,
571 ResponseConstructionError,
572> {
573 use bytes::Bytes;
574 use http_body_util::BodyExt;
575 use http_body_util::Full;
576
577 let status = response.status();
578 let code = status.as_u16();
579 let hyper_status = hyper::StatusCode::from_u16(code)
580 .map_err(|_| ResponseConstructionError::InvalidStatus(code))?;
581
582 let mut builder = hyper::Response::builder().status(hyper_status);
583 for field in response.head.headers().iter() {
584 builder = builder.header(field.name.as_str(), field.value.as_str());
585 }
586
587 let body = match response.body {
588 Some(ResponseBody::Empty) => Full::new(Bytes::new())
589 .map_err(|never| match never {})
590 .boxed(),
591 Some(ResponseBody::Bytes(b)) => Full::new(Bytes::from(b))
592 .map_err(|never| match never {})
593 .boxed(),
594 Some(ResponseBody::File(source)) => {
595 let permit = semaphore
596 .map(|s| s.clone().try_acquire_owned())
597 .transpose()
598 .map_err(|_| ResponseConstructionError::FileStreamLimit)?;
599 let permit = permit.map(CountingFileStreamPermit::new);
600 file_body(source, permit)
601 }
602 Some(ResponseBody::EmptyWithLength(_)) => Full::new(Bytes::new())
603 .map_err(|never| match never {})
604 .boxed(),
605 None => Full::new(Bytes::new())
606 .map_err(|never| match never {})
607 .boxed(),
608 };
609
610 let mut response = builder
611 .body(body)
612 .map_err(|_| ResponseConstructionError::InvalidHeader(HeaderError::InvalidValue))?;
613 crate::response::finalize_origin_headers(&mut response, std::time::SystemTime::now());
614 Ok(response)
615}
616
617fn file_body(
618 source: BodySource,
619 permit: Option<CountingFileStreamPermit>,
620) -> http_body_util::combinators::BoxBody<bytes::Bytes, std::io::Error> {
621 use bytes::Bytes;
622 use futures_util::stream;
623 use http_body_util::{BodyExt, StreamBody};
624 use hyper::body::Frame;
625 use tokio::io::{AsyncReadExt, AsyncSeekExt};
626
627 let (file, start, remaining) = match source {
628 BodySource::FileFull { file, len, .. } => (tokio::fs::File::from_std(file), 0, len),
629 BodySource::FileRange { file, range, .. } => {
630 (tokio::fs::File::from_std(file), range.start, range.len())
631 }
632 BodySource::Empty => {
633 return http_body_util::Full::new(Bytes::new())
634 .map_err(|never| match never {})
635 .boxed();
636 }
637 BodySource::Bytes(bytes) => {
638 return http_body_util::Full::new(Bytes::from(bytes))
639 .map_err(|never| match never {})
640 .boxed();
641 }
642 };
643
644 let stream = stream::unfold(
645 (file, start, remaining, permit),
646 |(mut file, offset, remaining, permit)| async move {
647 if remaining == 0 {
648 return None;
649 }
650 if offset > 0 {
651 if let Err(error) = file.seek(std::io::SeekFrom::Start(offset)).await {
652 return Some((Err(error), (file, offset, 0, permit)));
653 }
654 }
655 let chunk_len = remaining.min(64 * 1024) as usize;
656 let mut buffer = vec![0; chunk_len];
657 match file.read_exact(&mut buffer).await {
658 Ok(_) => Some((
659 Ok(Frame::data(Bytes::from(buffer))),
660 (
661 file,
662 offset + chunk_len as u64,
663 remaining - chunk_len as u64,
664 permit,
665 ),
666 )),
667 Err(error) => Some((Err(error), (file, offset, 0, permit))),
668 }
669 },
670 );
671 StreamBody::new(stream).boxed()
672}
673
674struct CountingFileStreamPermit {
675 _permit: tokio::sync::OwnedSemaphorePermit,
676}
677
678impl CountingFileStreamPermit {
679 fn new(permit: tokio::sync::OwnedSemaphorePermit) -> Self {
680 crate::ops::global_counters()
681 .active_file_streams
682 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
683 Self { _permit: permit }
684 }
685}
686
687impl Drop for CountingFileStreamPermit {
688 fn drop(&mut self) {
689 crate::ops::global_counters()
690 .active_file_streams
691 .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use crate::primitives::FileRange;
699 use http_body_util::BodyExt;
700 use std::fs::File;
701 use std::sync::Arc;
702 use tempfile::TempDir;
703
704 fn file_response(path: &std::path::Path, range: Option<FileRange>) -> Response {
705 let file = File::open(path).unwrap();
706 let metadata = file.metadata().unwrap();
707 let source = match range {
708 Some(range) => BodySource::FileRange {
709 file,
710 range,
711 total_len: metadata.len(),
712 mime: "application/octet-stream",
713 },
714 None => BodySource::FileFull {
715 file,
716 len: metadata.len(),
717 mime: "application/octet-stream",
718 },
719 };
720 Response::builder()
721 .status(StatusCode::OK)
722 .body(ResponseBody::File(source))
723 .unwrap()
724 }
725
726 #[tokio::test]
727 async fn full_file_transport_body_owns_permit_until_drop() {
728 let tmp = TempDir::new().unwrap();
729 let path = tmp.path().join("full.bin");
730 std::fs::write(&path, b"full body").unwrap();
731 let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
732
733 let first =
734 to_hyper_response_with_file_stream_semaphore(file_response(&path, None), &semaphore)
735 .unwrap();
736 assert!(matches!(
737 to_hyper_response_with_file_stream_semaphore(file_response(&path, None), &semaphore),
738 Err(ResponseConstructionError::FileStreamLimit)
739 ));
740
741 drop(first);
742 assert!(to_hyper_response_with_file_stream_semaphore(
743 file_response(&path, None),
744 &semaphore
745 )
746 .is_ok());
747 }
748
749 #[tokio::test]
750 async fn range_file_transport_body_releases_permit_on_completion() {
751 let tmp = TempDir::new().unwrap();
752 let path = tmp.path().join("range.bin");
753 std::fs::write(&path, b"range body").unwrap();
754 let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
755
756 let response = to_hyper_response_with_file_stream_semaphore(
757 file_response(&path, Some(FileRange::new(0, 4))),
758 &semaphore,
759 )
760 .unwrap();
761 let body = response.into_body().collect().await.unwrap().to_bytes();
762 assert_eq!(&body[..], b"range");
763 assert!(to_hyper_response_with_file_stream_semaphore(
764 file_response(&path, Some(FileRange::new(5, 9))),
765 &semaphore
766 )
767 .is_ok());
768 }
769
770 #[test]
771 fn non_file_and_normalized_head_bodies_bypass_file_admission() {
772 let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
773 let held = semaphore.clone().try_acquire_owned().unwrap();
774
775 for body in [
776 ResponseBody::Bytes(b"bytes".to_vec()),
777 ResponseBody::Empty,
778 ResponseBody::EmptyWithLength(5),
779 ] {
780 let response = Response::builder()
781 .status(StatusCode::OK)
782 .body(body)
783 .unwrap();
784 assert!(to_hyper_response_with_file_stream_semaphore(response, &semaphore).is_ok());
785 }
786
787 let tmp = TempDir::new().unwrap();
788 let path = tmp.path().join("head.bin");
789 std::fs::write(&path, b"head body").unwrap();
790 let normalized =
791 normalize_response(file_response(&path, None), &NormalizeRequest::new(true)).unwrap();
792 assert!(to_hyper_response_with_file_stream_semaphore(normalized, &semaphore).is_ok());
793
794 drop(held);
795 }
796
797 #[test]
798 fn status_code_valid_range() {
799 assert!(StatusCode::new(100).is_ok());
800 assert!(StatusCode::new(200).is_ok());
801 assert!(StatusCode::new(600).is_err());
802 }
803
804 #[test]
805 fn status_code_zero_rejected() {
806 assert!(StatusCode::new(0).is_err());
807 }
808
809 #[test]
810 fn status_code_below_100_rejected() {
811 assert!(StatusCode::new(1).is_err());
812 assert!(StatusCode::new(42).is_err());
813 assert!(StatusCode::new(99).is_err());
814 }
815
816 #[test]
817 fn status_code_over_599_rejected() {
818 assert!(StatusCode::new(600).is_err());
819 assert!(StatusCode::new(1000).is_err());
820 }
821
822 #[test]
823 fn status_code_boundary_values() {
824 assert!(StatusCode::new(100).is_ok());
825 assert!(StatusCode::new(199).is_ok());
826 assert!(StatusCode::new(200).is_ok());
827 assert!(StatusCode::new(599).is_ok());
828 }
829
830 #[test]
831 fn status_code_classification() {
832 assert!(StatusCode::CONTINUE.is_informational());
833 assert!(!StatusCode::OK.is_informational());
834 assert!(StatusCode::OK.is_success());
835 assert!(StatusCode::NOT_MODIFIED.is_redirection());
836 assert!(StatusCode::BAD_REQUEST.is_client_error());
837 assert!(StatusCode::INTERNAL_SERVER_ERROR.is_server_error());
838 }
839
840 #[test]
841 fn status_code_permits_payload() {
842 assert!(!StatusCode::CONTINUE.permits_payload_body());
843 assert!(!StatusCode::NO_CONTENT.permits_payload_body());
844 assert!(!StatusCode::NOT_MODIFIED.permits_payload_body());
845 assert!(!StatusCode::new(205).unwrap().permits_payload_body());
846 assert!(StatusCode::OK.permits_payload_body());
847 assert!(StatusCode::RANGE_NOT_SATISFIABLE.permits_payload_body());
848 }
849
850 #[test]
851 fn response_body_len() {
852 assert_eq!(ResponseBody::Empty.len(), 0);
853 assert_eq!(ResponseBody::Bytes(b"hello".to_vec()).len(), 5);
854 }
855
856 #[test]
857 fn response_body_into_bytes() {
858 assert!(ResponseBody::Empty.into_bytes().is_none());
859 assert_eq!(
860 ResponseBody::Bytes(b"hi".to_vec()).into_bytes(),
861 Some(b"hi".to_vec())
862 );
863 }
864
865 #[test]
866 fn response_builder_creates_response() {
867 let resp = Response::builder()
868 .status(StatusCode::OK)
869 .header("content-type", "text/plain")
870 .unwrap()
871 .body(ResponseBody::Bytes(b"ok".to_vec()))
872 .unwrap();
873
874 assert_eq!(resp.status().as_u16(), 200);
875 assert_eq!(
876 resp.headers().get_first("content-type").unwrap().as_str(),
877 "text/plain"
878 );
879 }
880
881 #[test]
882 fn response_builder_empty_body() {
883 let resp = Response::builder()
884 .status(StatusCode::NO_CONTENT)
885 .empty()
886 .unwrap();
887 assert_eq!(resp.status().as_u16(), 204);
888 assert!(resp.body().unwrap().is_empty());
889 }
890
891 #[test]
892 fn response_builder_no_status_returns_error() {
893 let result = Response::builder()
894 .header("content-type", "text/plain")
895 .unwrap()
896 .empty();
897 assert!(result.is_err());
898 }
899
900 #[test]
901 fn response_builder_invalid_header_name_rejected() {
902 let result = Response::builder()
903 .status(StatusCode::OK)
904 .header("", "value");
905 assert!(result.is_err());
906 }
907
908 #[test]
909 fn response_builder_invalid_header_value_rejected() {
910 let result = Response::builder()
911 .status(StatusCode::OK)
912 .header("x-test", "val\r\ninjection");
913 assert!(result.is_err());
914 }
915
916 #[test]
917 fn normalize_head_suppresses_body() {
918 let resp = Response::builder()
919 .status(StatusCode::OK)
920 .header("content-length", "5")
921 .unwrap()
922 .body(ResponseBody::Bytes(b"hello".to_vec()))
923 .unwrap();
924
925 let req = NormalizeRequest::new(true);
926 let normalized = normalize_response(resp, &req).unwrap();
927 assert!(normalized.body().unwrap().is_empty());
928 }
930
931 #[test]
932 fn normalize_304_suppresses_body() {
933 let resp = Response::builder()
934 .status(StatusCode::NOT_MODIFIED)
935 .header("etag", "W/\"123\"")
936 .unwrap()
937 .body(ResponseBody::Empty)
938 .unwrap();
939
940 let req = NormalizeRequest::new(false);
941 let normalized = normalize_response(resp, &req).unwrap();
942 assert_eq!(normalized.status().as_u16(), 304);
943 assert!(normalized.body().unwrap().is_empty());
944 }
945
946 #[test]
947 fn normalize_304_preserves_only_matching_content_length() {
948 let matching = Response::builder()
949 .status(StatusCode::NOT_MODIFIED)
950 .header("content-length", "5")
951 .unwrap()
952 .body(ResponseBody::Bytes(b"hello".to_vec()))
953 .unwrap();
954 let normalized = normalize_response(matching, &NormalizeRequest::new(false)).unwrap();
955 assert_eq!(
956 normalized
957 .headers()
958 .get_first("content-length")
959 .unwrap()
960 .as_str(),
961 "5"
962 );
963
964 let mismatched = Response::builder()
965 .status(StatusCode::NOT_MODIFIED)
966 .header("content-length", "4")
967 .unwrap()
968 .body(ResponseBody::Bytes(b"hello".to_vec()))
969 .unwrap();
970 let normalized = normalize_response(mismatched, &NormalizeRequest::new(false)).unwrap();
971 assert!(!normalized.headers().contains("content-length"));
972 }
973
974 #[test]
975 fn normalize_204_suppresses_body() {
976 let resp = Response::builder()
977 .status(StatusCode::NO_CONTENT)
978 .body(ResponseBody::Bytes(b"unexpected".to_vec()))
979 .unwrap();
980
981 let req = NormalizeRequest::new(false);
982 let normalized = normalize_response(resp, &req).unwrap();
983 assert!(normalized.body().unwrap().is_empty());
984 }
985
986 #[test]
987 fn normalize_205_suppresses_body_and_content_length() {
988 let resp = Response::builder()
989 .status(StatusCode::RESET_CONTENT)
990 .body(ResponseBody::Bytes(b"unexpected".to_vec()))
991 .unwrap();
992 let normalized = normalize_response(resp, &NormalizeRequest::new(false)).unwrap();
993 assert!(normalized.body().unwrap().is_empty());
994 assert!(!normalized.headers().contains("content-length"));
995 }
996
997 #[test]
998 fn normalize_strips_transfer_encoding() {
999 let resp = Response::builder()
1000 .status(StatusCode::OK)
1001 .header("transfer-encoding", "chunked")
1002 .unwrap()
1003 .body(ResponseBody::Bytes(b"hello".to_vec()))
1004 .unwrap();
1005
1006 let req = NormalizeRequest::new(false);
1007 let normalized = normalize_response(resp, &req).unwrap();
1008 assert!(!normalized.headers().contains("transfer-encoding"));
1009 }
1010
1011 #[test]
1012 fn normalize_sets_content_length() {
1013 let resp = Response::builder()
1014 .status(StatusCode::OK)
1015 .body(ResponseBody::Bytes(b"hello".to_vec()))
1016 .unwrap();
1017
1018 let req = NormalizeRequest::new(false);
1019 let normalized = normalize_response(resp, &req).unwrap();
1020 assert_eq!(
1021 normalized
1022 .headers()
1023 .get_first("content-length")
1024 .unwrap()
1025 .as_str(),
1026 "5"
1027 );
1028 }
1029
1030 #[test]
1031 fn normalize_1xx_suppresses_body() {
1032 let resp = Response::builder()
1033 .status(StatusCode::CONTINUE)
1034 .body(ResponseBody::Bytes(b"data".to_vec()))
1035 .unwrap();
1036
1037 let req = NormalizeRequest::new(false);
1038 let normalized = normalize_response(resp, &req).unwrap();
1039 assert!(normalized.body().unwrap().is_empty());
1040 }
1041
1042 #[test]
1043 fn normalize_duplicate_headers_preserved() {
1044 let mut resp = Response::builder()
1045 .status(StatusCode::OK)
1046 .body(ResponseBody::Bytes(b"ok".to_vec()))
1047 .unwrap();
1048 resp.head.headers.push_str("set-cookie", "a=1").unwrap();
1049 resp.head.headers.push_str("set-cookie", "b=2").unwrap();
1050
1051 let req = NormalizeRequest::new(false);
1052 let normalized = normalize_response(resp, &req).unwrap();
1053 let all = normalized.headers().get_all("set-cookie");
1054 assert_eq!(all.len(), 2);
1055 }
1056
1057 #[test]
1058 fn response_construction_error_display() {
1059 let err = ResponseConstructionError::InvalidStatus(0);
1060 assert!(err.to_string().contains("0"));
1061
1062 let err = ResponseConstructionError::ForbiddenFramingHeader("transfer-encoding".into());
1063 assert!(err.to_string().contains("transfer-encoding"));
1064
1065 let err = ResponseConstructionError::BodyAlreadyConsumed;
1066 assert!(!err.to_string().is_empty());
1067
1068 let err = ResponseConstructionError::ContentLengthMismatch {
1069 declared: 100,
1070 actual: 50,
1071 };
1072 assert!(err.to_string().contains("100"));
1073 assert!(err.to_string().contains("50"));
1074 }
1075
1076 #[test]
1077 fn status_code_display() {
1078 assert_eq!(format!("{}", StatusCode::OK), "200");
1079 assert_eq!(format!("{}", StatusCode::NOT_FOUND), "404");
1080 }
1081
1082 #[test]
1083 fn status_code_into_u16() {
1084 let code: u16 = StatusCode::OK.into();
1085 assert_eq!(code, 200);
1086 }
1087
1088 #[test]
1089 fn is_hop_by_hop_header_recognizes_all_variants() {
1090 assert!(is_hop_by_hop_header("connection"));
1091 assert!(is_hop_by_hop_header("Connection"));
1092 assert!(is_hop_by_hop_header("CONNECTION"));
1093 assert!(is_hop_by_hop_header("keep-alive"));
1094 assert!(is_hop_by_hop_header("Keep-Alive"));
1095 assert!(is_hop_by_hop_header("proxy-authenticate"));
1096 assert!(is_hop_by_hop_header("proxy-authorization"));
1097 assert!(is_hop_by_hop_header("proxy-connection"));
1098 assert!(is_hop_by_hop_header("te"));
1099 assert!(is_hop_by_hop_header("TE"));
1100 assert!(is_hop_by_hop_header("trailer"));
1101 assert!(is_hop_by_hop_header("Trailer"));
1102 assert!(is_hop_by_hop_header("transfer-encoding"));
1103 assert!(is_hop_by_hop_header("Transfer-Encoding"));
1104 assert!(is_hop_by_hop_header("upgrade"));
1105 assert!(is_hop_by_hop_header("Upgrade"));
1106 }
1107
1108 #[test]
1109 fn is_hop_by_hop_header_rejects_end_to_end() {
1110 assert!(!is_hop_by_hop_header("content-type"));
1111 assert!(!is_hop_by_hop_header("content-length"));
1112 assert!(!is_hop_by_hop_header("host"));
1113 assert!(!is_hop_by_hop_header("set-cookie"));
1114 assert!(!is_hop_by_hop_header("etag"));
1115 assert!(!is_hop_by_hop_header("authorization"));
1116 assert!(!is_hop_by_hop_header("cache-control"));
1117 }
1118
1119 #[test]
1120 fn normalize_metadata_strips_all_hop_by_hop() {
1121 let code = StatusCode::OK;
1122 let mut headers = HeaderBlock::new();
1123 headers.push_str("content-type", "text/plain").unwrap();
1124 headers.push_str("transfer-encoding", "chunked").unwrap();
1125 headers.push_str("connection", "keep-alive").unwrap();
1126 headers.push_str("trailer", "x-checksum").unwrap();
1127 headers.push_str("upgrade", "h2c").unwrap();
1128 headers.push_str("te", "deflate").unwrap();
1129
1130 normalize_metadata(code, &mut headers, 5, false).unwrap();
1131
1132 assert!(!headers.contains("transfer-encoding"));
1133 assert!(!headers.contains("connection"));
1134 assert!(!headers.contains("trailer"));
1135 assert!(!headers.contains("upgrade"));
1136 assert!(!headers.contains("te"));
1137 assert!(headers.contains("content-type"));
1138 assert_eq!(headers.get_first("content-length").unwrap().as_str(), "5");
1139 }
1140
1141 #[test]
1142 fn duplicate_content_length_replaced_by_normalized_value() {
1143 let code = StatusCode::OK;
1144 let mut headers = HeaderBlock::new();
1145 headers.push_str("content-length", "999").unwrap();
1146 headers.push_str("content-length", "888").unwrap();
1147
1148 normalize_metadata(code, &mut headers, 42, false).unwrap();
1149
1150 let all_cl = headers.get_all("content-length");
1151 assert_eq!(all_cl.len(), 1, "only one Content-Length must remain");
1152 assert_eq!(all_cl[0].as_str(), "42");
1153 }
1154
1155 #[test]
1156 fn transfer_encoding_plus_content_length_strips_te() {
1157 let resp = Response::builder()
1158 .status(StatusCode::OK)
1159 .header("transfer-encoding", "chunked")
1160 .unwrap()
1161 .header("content-length", "100")
1162 .unwrap()
1163 .body(ResponseBody::Bytes(b"hello".to_vec()))
1164 .unwrap();
1165
1166 let req = NormalizeRequest::new(false);
1167 let normalized = normalize_response(resp, &req).unwrap();
1168
1169 assert!(!normalized.headers().contains("transfer-encoding"));
1170 assert_eq!(
1171 normalized
1172 .headers()
1173 .get_first("content-length")
1174 .unwrap()
1175 .as_str(),
1176 "5"
1177 );
1178 }
1179
1180 #[test]
1181 fn normalize_metadata_preserves_duplicate_set_cookie() {
1182 let code = StatusCode::OK;
1183 let mut headers = HeaderBlock::new();
1184 headers.push_str("set-cookie", "a=1").unwrap();
1185 headers.push_str("set-cookie", "b=2").unwrap();
1186
1187 normalize_metadata(code, &mut headers, 0, false).unwrap();
1188
1189 let all = headers.get_all("set-cookie");
1190 assert_eq!(all.len(), 2);
1191 assert_eq!(all[0].as_str(), "a=1");
1192 assert_eq!(all[1].as_str(), "b=2");
1193 }
1194
1195 #[test]
1196 fn normalize_metadata_head_preserves_content_length_when_body_nonempty() {
1197 let code = StatusCode::OK;
1198 let mut headers = HeaderBlock::new();
1199 headers.push_str("content-length", "100").unwrap();
1200
1201 normalize_metadata(code, &mut headers, 100, true).unwrap();
1202
1203 assert_eq!(
1204 headers.get_first("content-length").unwrap().as_str(),
1205 "100",
1206 "HEAD with non-empty body must preserve Content-Length"
1207 );
1208 }
1209
1210 #[test]
1211 fn normalize_metadata_head_suppresses_content_length_when_body_empty() {
1212 let code = StatusCode::OK;
1213 let mut headers = HeaderBlock::new();
1214 headers.push_str("content-length", "100").unwrap();
1215
1216 normalize_metadata(code, &mut headers, 0, true).unwrap();
1217
1218 assert!(
1219 !headers.contains("content-length"),
1220 "HEAD with empty body must suppress Content-Length"
1221 );
1222 }
1223}