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