Skip to main content

a3s_boot/http/
response.rs

1use super::cookie::CookieOptions;
2use super::header::{
3    get_header, is_json_media_type, matches_media_type, normalize_header_name, normalize_headers,
4    parse_content_length, strict_content_length_values, validate_header_name,
5    validate_header_value,
6};
7use crate::{BootError, Result, SseEvent, SseStream};
8use futures_core::Stream;
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11use std::collections::BTreeMap;
12use std::fmt;
13use std::sync::{Arc, Mutex};
14
15/// Framework-neutral HTTP response returned by Boot route handlers.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct BootResponse {
18    pub status: u16,
19    pub headers: BTreeMap<String, String>,
20    pub appended_headers: Vec<(String, String)>,
21    pub body: Vec<u8>,
22    stream: Option<SharedSseStream>,
23}
24
25#[derive(Clone)]
26struct SharedSseStream {
27    inner: Arc<Mutex<Option<SseStream>>>,
28}
29
30impl SharedSseStream {
31    fn new(stream: SseStream) -> Self {
32        Self {
33            inner: Arc::new(Mutex::new(Some(stream))),
34        }
35    }
36
37    fn take(&self) -> Option<SseStream> {
38        self.inner.lock().ok()?.take()
39    }
40}
41
42impl fmt::Debug for SharedSseStream {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.debug_struct("SharedSseStream").finish_non_exhaustive()
45    }
46}
47
48impl PartialEq for SharedSseStream {
49    fn eq(&self, other: &Self) -> bool {
50        Arc::ptr_eq(&self.inner, &other.inner)
51    }
52}
53
54impl Eq for SharedSseStream {}
55
56impl Default for BootResponse {
57    fn default() -> Self {
58        Self::new(200, Vec::<u8>::new())
59    }
60}
61
62impl BootResponse {
63    pub fn new(status: u16, body: impl Into<Vec<u8>>) -> Self {
64        Self {
65            status,
66            headers: BTreeMap::new(),
67            appended_headers: Vec::new(),
68            body: body.into(),
69            stream: None,
70        }
71    }
72
73    pub fn status(&self) -> u16 {
74        self.status
75    }
76
77    pub fn body(&self) -> &[u8] {
78        &self.body
79    }
80
81    pub fn into_body(self) -> Vec<u8> {
82        self.body
83    }
84
85    pub fn empty(status: u16) -> Self {
86        Self::new(status, Vec::<u8>::new())
87    }
88
89    pub fn no_content() -> Self {
90        Self::empty(204)
91    }
92
93    pub fn redirect(location: impl Into<String>) -> Self {
94        Self::redirect_with_status(302, location)
95    }
96
97    pub fn see_other(location: impl Into<String>) -> Self {
98        Self::redirect_with_status(303, location)
99    }
100
101    pub fn temporary_redirect(location: impl Into<String>) -> Self {
102        Self::redirect_with_status(307, location)
103    }
104
105    pub fn permanent_redirect(location: impl Into<String>) -> Self {
106        Self::redirect_with_status(308, location)
107    }
108
109    pub fn redirect_with_status(status: u16, location: impl Into<String>) -> Self {
110        Self::empty(status).with_location(location)
111    }
112
113    pub fn text(body: impl Into<String>) -> Self {
114        Self::text_with_status(200, body)
115    }
116
117    pub fn text_with_status(status: u16, body: impl Into<String>) -> Self {
118        Self::new(status, body.into()).with_header("content-type", "text/plain; charset=utf-8")
119    }
120
121    pub fn json<T>(body: &T) -> Result<Self>
122    where
123        T: Serialize,
124    {
125        Self::json_with_status(200, body)
126    }
127
128    pub fn json_with_status<T>(status: u16, body: &T) -> Result<Self>
129    where
130        T: Serialize,
131    {
132        let body = serde_json::to_vec(body).map_err(|err| BootError::Internal(err.to_string()))?;
133        Ok(Self::new(status, body).with_header("content-type", "application/json"))
134    }
135
136    pub fn sse<S>(stream: S) -> Self
137    where
138        S: Stream<Item = Result<SseEvent>> + Send + 'static,
139    {
140        Self::empty(200)
141            .with_header("content-type", "text/event-stream; charset=utf-8")
142            .with_header("cache-control", "no-cache")
143            .with_header("connection", "keep-alive")
144            .with_sse_stream(stream)
145    }
146
147    pub fn from_error(error: &BootError) -> Self {
148        Self::text_with_status(error.http_status_code(), error.http_response_message())
149    }
150
151    pub fn body_text(&self) -> Result<String> {
152        if self.is_streaming() {
153            return Err(BootError::Internal(
154                "streaming response body cannot be read as text".to_string(),
155            ));
156        }
157        String::from_utf8(self.body.clone()).map_err(|err| BootError::Internal(err.to_string()))
158    }
159
160    pub fn body_json<T>(&self) -> Result<T>
161    where
162        T: DeserializeOwned,
163    {
164        if self.is_streaming() {
165            return Err(BootError::Internal(
166                "streaming response body cannot be read as JSON".to_string(),
167            ));
168        }
169        serde_json::from_slice(&self.body).map_err(|err| BootError::Internal(err.to_string()))
170    }
171
172    pub fn with_status(mut self, status: u16) -> Self {
173        self.status = status;
174        self
175    }
176
177    pub fn with_content_type(self, content_type: impl Into<String>) -> Self {
178        self.with_header("content-type", content_type)
179    }
180
181    pub fn with_content_length(self, content_length: u64) -> Self {
182        self.with_header("content-length", content_length.to_string())
183    }
184
185    pub fn with_location(self, location: impl Into<String>) -> Self {
186        self.with_header("location", location)
187    }
188
189    pub fn with_www_authenticate(self, challenge: impl Into<String>) -> Self {
190        self.with_header("www-authenticate", challenge)
191    }
192
193    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
194        self.headers
195            .insert(normalize_header_name(name), value.into());
196        self
197    }
198
199    pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
200        self.headers = normalize_headers(headers);
201        self
202    }
203
204    pub fn append_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
205        self.appended_headers
206            .push((normalize_header_name(name), value.into()));
207        self
208    }
209
210    pub fn append_www_authenticate(self, challenge: impl Into<String>) -> Self {
211        self.append_header("www-authenticate", challenge)
212    }
213
214    pub fn with_cookie(
215        self,
216        name: impl AsRef<str>,
217        value: impl AsRef<str>,
218        options: CookieOptions,
219    ) -> Result<Self> {
220        let header = options.set_cookie_header(name.as_ref(), value.as_ref())?;
221        Ok(self.append_header("set-cookie", header))
222    }
223
224    pub fn delete_cookie(self, name: impl AsRef<str>, options: CookieOptions) -> Result<Self> {
225        let header = options.delete_cookie_header(name.as_ref())?;
226        Ok(self.append_header("set-cookie", header))
227    }
228
229    pub fn is_streaming(&self) -> bool {
230        self.stream.is_some()
231    }
232
233    pub fn is_event_stream(&self) -> bool {
234        self.is_content_type("text/event-stream")
235    }
236
237    pub fn into_sse_stream(self) -> Option<SseStream> {
238        self.stream.and_then(|stream| stream.take())
239    }
240
241    pub fn header_entries(&self) -> impl Iterator<Item = (&str, &str)> {
242        self.headers
243            .iter()
244            .map(|(name, value)| (name.as_str(), value.as_str()))
245            .chain(
246                self.appended_headers
247                    .iter()
248                    .map(|(name, value)| (name.as_str(), value.as_str())),
249            )
250    }
251
252    pub fn validate_headers(&self) -> Result<()> {
253        for (name, value) in self.header_entries() {
254            validate_response_header(name, value)?;
255        }
256
257        Ok(())
258    }
259
260    pub fn header(&self, name: &str) -> Option<&str> {
261        get_header(&self.headers, name)
262    }
263
264    pub fn header_values(&self, name: &str) -> Vec<&str> {
265        let mut values = self.header(name).into_iter().collect::<Vec<_>>();
266        values.extend(
267            self.appended_headers
268                .iter()
269                .filter(|(key, _)| key.eq_ignore_ascii_case(name))
270                .map(|(_, value)| value.as_str()),
271        );
272        values
273    }
274
275    pub fn content_type(&self) -> Option<&str> {
276        self.header_values("content-type").into_iter().next()
277    }
278
279    pub fn location(&self) -> Option<&str> {
280        self.header_values("location").into_iter().next()
281    }
282
283    pub fn www_authenticate(&self) -> Option<&str> {
284        self.header_values("www-authenticate").into_iter().next()
285    }
286
287    pub fn www_authenticate_values(&self) -> Vec<&str> {
288        self.header_values("www-authenticate")
289    }
290
291    pub fn content_length(&self) -> Result<Option<u64>> {
292        let Some(content_length) = self.header_values("content-length").into_iter().next() else {
293            return Ok(None);
294        };
295
296        parse_content_length(content_length)
297            .map(Some)
298            .ok_or_else(|| {
299                BootError::Internal(format!("invalid content-length header: {content_length}"))
300            })
301    }
302
303    pub fn strict_content_length(&self) -> Result<Option<u64>> {
304        strict_content_length_values(
305            self.header_values("content-length"),
306            |content_length| {
307                BootError::Internal(format!(
308                    "invalid response content-length header: {content_length}"
309                ))
310            },
311            |expected_content_length, content_length| {
312                BootError::Internal(format!(
313                    "conflicting response content-length headers: {expected_content_length} != {content_length}"
314                ))
315            },
316        )
317    }
318
319    pub fn validate_content_length(&self) -> Result<()> {
320        let Some(content_length) = self.strict_content_length()? else {
321            return Ok(());
322        };
323        if self.is_streaming() {
324            return Err(BootError::Internal(
325                "streaming responses must not include a content-length header".to_string(),
326            ));
327        }
328
329        let actual_body_length = self.body.len() as u64;
330        if actual_body_length == content_length {
331            return Ok(());
332        }
333
334        Err(BootError::Internal(format!(
335            "response content-length header does not match response body length: expected {content_length}, got {actual_body_length}"
336        )))
337    }
338
339    pub fn is_content_type(&self, media_type: &str) -> bool {
340        self.content_type()
341            .is_some_and(|content_type| matches_media_type(content_type, media_type))
342    }
343
344    pub fn is_json_content_type(&self) -> bool {
345        self.content_type().is_some_and(is_json_media_type)
346    }
347
348    pub fn has_body(&self) -> bool {
349        self.is_streaming() || !self.body.is_empty()
350    }
351
352    pub fn allows_body(&self) -> bool {
353        !(self.is_informational() || self.status == 204 || self.status == 304)
354    }
355
356    pub fn validate_body_allowed(&self) -> Result<()> {
357        if !self.has_body() || self.allows_body() {
358            return Ok(());
359        }
360
361        Err(BootError::Internal(format!(
362            "response status {} must not include a body",
363            self.status
364        )))
365    }
366
367    pub fn is_informational(&self) -> bool {
368        (100..200).contains(&self.status)
369    }
370
371    pub fn is_success(&self) -> bool {
372        (200..300).contains(&self.status)
373    }
374
375    pub fn is_redirection(&self) -> bool {
376        (300..400).contains(&self.status)
377    }
378
379    pub fn is_client_error(&self) -> bool {
380        (400..500).contains(&self.status)
381    }
382
383    pub fn is_server_error(&self) -> bool {
384        (500..600).contains(&self.status)
385    }
386
387    pub fn is_error(&self) -> bool {
388        self.is_client_error() || self.is_server_error()
389    }
390
391    pub fn is_valid_status(&self) -> bool {
392        (100..1000).contains(&self.status)
393    }
394
395    pub fn validate_status(&self) -> Result<()> {
396        if self.is_valid_status() {
397            return Ok(());
398        }
399
400        Err(BootError::Internal(format!(
401            "invalid response status {}",
402            self.status
403        )))
404    }
405
406    pub fn validate(&self) -> Result<()> {
407        self.validate_status()?;
408        self.validate_content_length()?;
409        self.validate_body_allowed()?;
410        self.validate_headers()
411    }
412
413    fn with_sse_stream<S>(mut self, stream: S) -> Self
414    where
415        S: Stream<Item = Result<SseEvent>> + Send + 'static,
416    {
417        self.stream = Some(SharedSseStream::new(Box::pin(stream)));
418        self
419    }
420}
421
422fn validate_response_header(name: &str, value: &str) -> Result<()> {
423    validate_header_name(name).map_err(|message| {
424        BootError::Internal(format!("invalid response header name {name:?}: {message}"))
425    })?;
426    validate_header_value(value).map_err(|message| {
427        BootError::Internal(format!(
428            "invalid response header value for {name:?}: {message}"
429        ))
430    })
431}