io_http/rfc9110/
response.rs1use core::{fmt, str};
6
7use alloc::{borrow::ToOwned, format, string::String, vec::Vec};
8
9use crate::rfc9110::{headers::SENSITIVE_HEADERS, status::StatusCode};
10
11#[derive(Clone)]
13pub struct HttpResponse {
14 pub status: StatusCode,
15 pub version: String,
16 pub headers: Vec<(String, String)>,
17 pub body: Vec<u8>,
18}
19
20impl HttpResponse {
21 pub fn header(&self, name: &str) -> Option<&str> {
23 self.headers
24 .iter()
25 .find(|(k, _)| k.eq_ignore_ascii_case(name))
26 .map(|(_, v)| v.as_str())
27 }
28}
29
30impl fmt::Debug for HttpResponse {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 let headers: Vec<(&str, &str)> = self
33 .headers
34 .iter()
35 .map(|(k, v)| {
36 let sensitive = SENSITIVE_HEADERS.iter().any(|s| k.eq_ignore_ascii_case(s));
37 let v = if sensitive { "[REDACTED]" } else { v.as_str() };
38 (k.as_str(), v)
39 })
40 .collect();
41
42 f.debug_struct("HttpResponse")
43 .field("status", &self.status)
44 .field("version", &self.version)
45 .field("headers", &headers)
46 .field(
47 "body",
48 &match str::from_utf8(&self.body) {
49 Ok(body) => body.to_owned(),
50 Err(_) => format!("[{} BYTES]", self.body.len()),
51 },
52 )
53 .finish()
54 }
55}
56
57#[derive(Clone, Debug)]
59pub(crate) struct ResponseBuilder {
60 pub(crate) status: Option<StatusCode>,
61 pub(crate) version: String,
62 pub(crate) headers: Vec<(String, String)>,
63}
64
65impl Default for ResponseBuilder {
66 fn default() -> Self {
67 Self {
68 status: None,
69 version: "HTTP/1.1".into(),
70 headers: Vec::new(),
71 }
72 }
73}
74
75impl ResponseBuilder {
76 pub(crate) fn header(&mut self, name: &str, value: &[u8]) {
77 let value = String::from_utf8_lossy(value).into_owned();
78 self.headers.push((name.to_lowercase(), value));
79 }
80
81 pub(crate) fn get_header(&self, name: &str) -> Option<&str> {
82 self.headers
83 .iter()
84 .find(|(k, _)| k.eq_ignore_ascii_case(name))
85 .map(|(_, v)| v.as_str())
86 }
87
88 pub(crate) fn build(self, body: Vec<u8>) -> HttpResponse {
89 HttpResponse {
90 status: self.status.unwrap_or(StatusCode(200)),
91 version: self.version,
92 headers: self.headers,
93 body,
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use alloc::vec;
101
102 use crate::rfc9110::response::*;
103
104 #[test]
105 fn header_case_insensitive() {
106 let response = HttpResponse {
107 status: StatusCode(200),
108 version: String::new(),
109 headers: vec![("Content-Type".into(), "text/html".into())],
110 body: vec![],
111 };
112 assert_eq!(response.header("content-type"), Some("text/html"));
113 assert_eq!(response.header("CONTENT-TYPE"), Some("text/html"));
114 assert_eq!(response.header("Content-Type"), Some("text/html"));
115 }
116
117 #[test]
118 fn header_missing_returns_none() {
119 let response = HttpResponse {
120 status: StatusCode(200),
121 version: String::new(),
122 headers: vec![],
123 body: vec![],
124 };
125 assert_eq!(response.header("x-missing"), None);
126 }
127
128 #[test]
129 fn header_returns_first_match() {
130 let response = HttpResponse {
131 status: StatusCode(200),
132 version: String::new(),
133 headers: vec![
134 ("X-Foo".into(), "first".into()),
135 ("x-foo".into(), "second".into()),
136 ],
137 body: vec![],
138 };
139 assert_eq!(response.header("x-foo"), Some("first"));
140 }
141
142 #[test]
143 fn builder_stores_headers_lowercase() {
144 let mut builder = ResponseBuilder::default();
145 builder.header("Content-Type", b"text/plain");
146 assert_eq!(builder.headers[0].0, "content-type");
147 }
148
149 #[test]
150 fn builder_get_header_case_insensitive() {
151 let mut builder = ResponseBuilder::default();
152 builder.header("Content-Type", b"text/html");
153 assert_eq!(builder.get_header("Content-Type"), Some("text/html"));
154 assert_eq!(builder.get_header("content-type"), Some("text/html"));
155 assert_eq!(builder.get_header("CONTENT-TYPE"), Some("text/html"));
156 }
157
158 #[test]
159 fn builder_build_defaults_to_200() {
160 let response = ResponseBuilder::default().build(vec![]);
161 assert_eq!(*response.status, 200);
162 }
163
164 #[test]
165 fn builder_default_version_is_http11() {
166 let response = ResponseBuilder::default().build(vec![]);
167 assert_eq!(response.version, "HTTP/1.1");
168 }
169
170 #[test]
171 fn builder_build_transfers_fields() {
172 let mut builder = ResponseBuilder::default();
173 builder.status = Some(StatusCode(404));
174 builder.header("X-Custom", b"value");
175 let response = builder.build(b"not found".to_vec());
176 assert_eq!(*response.status, 404);
177 assert_eq!(response.header("x-custom"), Some("value"));
178 assert_eq!(response.body, b"not found");
179 }
180}