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 super::streamable_file::{StreamableFile, StreamableFileBody, StreamableFileStream};
8use crate::{BootError, Result, SseEvent, SseStream};
9use futures_core::Stream;
10use serde::de::DeserializeOwned;
11use serde::Serialize;
12use std::collections::BTreeMap;
13use std::fmt;
14use std::sync::{Arc, Mutex};
15
16/// Framework-neutral HTTP response returned by Boot route handlers.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct BootResponse {
19    pub status: u16,
20    pub headers: BTreeMap<String, String>,
21    pub appended_headers: Vec<(String, String)>,
22    pub body: Vec<u8>,
23    stream: Option<SharedResponseStream>,
24}
25
26#[derive(Clone)]
27struct SharedResponseStream {
28    kind: ResponseStreamKind,
29    inner: Arc<Mutex<Option<ResponseStream>>>,
30}
31
32impl SharedResponseStream {
33    fn new(stream: ResponseStream) -> Self {
34        let kind = stream.kind();
35        Self {
36            kind,
37            inner: Arc::new(Mutex::new(Some(stream))),
38        }
39    }
40
41    fn kind(&self) -> ResponseStreamKind {
42        self.kind
43    }
44
45    fn take(&self) -> Option<SseStream> {
46        let mut guard = self.inner.lock().ok()?;
47        match guard.take()? {
48            ResponseStream::Sse(stream) => Some(stream),
49            ResponseStream::Body(stream) => {
50                *guard = Some(ResponseStream::Body(stream));
51                None
52            }
53        }
54    }
55
56    fn take_body(&self) -> Option<StreamableFileStream> {
57        let mut guard = self.inner.lock().ok()?;
58        match guard.take()? {
59            ResponseStream::Body(stream) => Some(stream),
60            ResponseStream::Sse(stream) => {
61                *guard = Some(ResponseStream::Sse(stream));
62                None
63            }
64        }
65    }
66}
67
68impl fmt::Debug for SharedResponseStream {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.debug_struct("SharedResponseStream")
71            .field("kind", &self.kind)
72            .finish_non_exhaustive()
73    }
74}
75
76impl PartialEq for SharedResponseStream {
77    fn eq(&self, other: &Self) -> bool {
78        Arc::ptr_eq(&self.inner, &other.inner)
79    }
80}
81
82impl Eq for SharedResponseStream {}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85enum ResponseStreamKind {
86    Sse,
87    Body,
88}
89
90enum ResponseStream {
91    Sse(SseStream),
92    Body(StreamableFileStream),
93}
94
95#[derive(Debug, Serialize)]
96struct HttpErrorResponseBody {
97    #[serde(rename = "statusCode")]
98    status_code: u16,
99    message: String,
100    error: &'static str,
101}
102
103impl ResponseStream {
104    fn kind(&self) -> ResponseStreamKind {
105        match self {
106            Self::Sse(_) => ResponseStreamKind::Sse,
107            Self::Body(_) => ResponseStreamKind::Body,
108        }
109    }
110}
111
112impl Default for BootResponse {
113    fn default() -> Self {
114        Self::new(200, Vec::<u8>::new())
115    }
116}
117
118impl BootResponse {
119    pub fn new(status: u16, body: impl Into<Vec<u8>>) -> Self {
120        Self {
121            status,
122            headers: BTreeMap::new(),
123            appended_headers: Vec::new(),
124            body: body.into(),
125            stream: None,
126        }
127    }
128
129    pub fn status(&self) -> u16 {
130        self.status
131    }
132
133    pub fn body(&self) -> &[u8] {
134        &self.body
135    }
136
137    pub fn into_body(self) -> Vec<u8> {
138        self.body
139    }
140
141    pub fn empty(status: u16) -> Self {
142        Self::new(status, Vec::<u8>::new())
143    }
144
145    pub fn no_content() -> Self {
146        Self::empty(204)
147    }
148
149    pub fn redirect(location: impl Into<String>) -> Self {
150        Self::redirect_with_status(302, location)
151    }
152
153    pub fn see_other(location: impl Into<String>) -> Self {
154        Self::redirect_with_status(303, location)
155    }
156
157    pub fn temporary_redirect(location: impl Into<String>) -> Self {
158        Self::redirect_with_status(307, location)
159    }
160
161    pub fn permanent_redirect(location: impl Into<String>) -> Self {
162        Self::redirect_with_status(308, location)
163    }
164
165    pub fn redirect_with_status(status: u16, location: impl Into<String>) -> Self {
166        Self::empty(status).with_location(location)
167    }
168
169    pub fn text(body: impl Into<String>) -> Self {
170        Self::text_with_status(200, body)
171    }
172
173    pub fn text_with_status(status: u16, body: impl Into<String>) -> Self {
174        Self::new(status, body.into()).with_header("content-type", "text/plain; charset=utf-8")
175    }
176
177    pub fn html(body: impl Into<String>) -> Self {
178        Self::html_with_status(200, body)
179    }
180
181    pub fn html_with_status(status: u16, body: impl Into<String>) -> Self {
182        Self::new(status, body.into()).with_header("content-type", "text/html; charset=utf-8")
183    }
184
185    pub fn json<T>(body: &T) -> Result<Self>
186    where
187        T: Serialize,
188    {
189        Self::json_with_status(200, body)
190    }
191
192    pub fn json_with_status<T>(status: u16, body: &T) -> Result<Self>
193    where
194        T: Serialize,
195    {
196        let body = serde_json::to_vec(body).map_err(|err| BootError::Internal(err.to_string()))?;
197        Ok(Self::new(status, body).with_header("content-type", "application/json"))
198    }
199
200    pub fn sse<S>(stream: S) -> Self
201    where
202        S: Stream<Item = Result<SseEvent>> + Send + 'static,
203    {
204        Self::empty(200)
205            .with_header("content-type", "text/event-stream; charset=utf-8")
206            .with_header("cache-control", "no-cache")
207            .with_header("connection", "keep-alive")
208            .with_sse_stream(stream)
209    }
210
211    pub fn streamable_file(file: StreamableFile) -> Self {
212        let (body, options) = file.into_parts();
213        let mut response = match body {
214            StreamableFileBody::Bytes(body) => {
215                let content_length = options.content_length().unwrap_or(body.len() as u64);
216                Self::new(200, body).with_content_length(content_length)
217            }
218            StreamableFileBody::Stream(stream) => {
219                let response = Self::empty(200).with_body_stream(stream);
220                if let Some(content_length) = options.content_length() {
221                    response.with_content_length(content_length)
222                } else {
223                    response
224                }
225            }
226        }
227        .with_content_type(options.content_type().unwrap_or("application/octet-stream"));
228
229        if let Some(content_disposition) = options.content_disposition() {
230            response = response.with_header("content-disposition", content_disposition);
231        }
232
233        response
234    }
235
236    pub fn file(body: impl Into<Vec<u8>>) -> Self {
237        Self::streamable_file(StreamableFile::bytes(body))
238    }
239
240    pub fn download(file_name: impl AsRef<str>, body: impl Into<Vec<u8>>) -> Result<Self> {
241        Ok(Self::streamable_file(
242            StreamableFile::bytes(body).with_attachment(file_name)?,
243        ))
244    }
245
246    pub fn download_stream<S>(file_name: impl AsRef<str>, stream: S) -> Result<Self>
247    where
248        S: Stream<Item = Result<Vec<u8>>> + Send + 'static,
249    {
250        Ok(Self::streamable_file(
251            StreamableFile::stream(stream).with_attachment(file_name)?,
252        ))
253    }
254
255    pub fn from_error(error: &BootError) -> Self {
256        let status = error.http_status_code();
257        let body = HttpErrorResponseBody {
258            status_code: status,
259            message: error.http_response_message(),
260            error: http_error_name(status),
261        };
262        let body = serde_json::to_vec(&body).unwrap_or_else(|_| {
263            br#"{"statusCode":500,"message":"failed to encode error response","error":"Internal Server Error"}"#.to_vec()
264        });
265
266        Self::new(status, body).with_header("content-type", "application/json")
267    }
268
269    pub fn body_text(&self) -> Result<String> {
270        if self.is_streaming() {
271            return Err(BootError::Internal(
272                "streaming response body cannot be read as text".to_string(),
273            ));
274        }
275        String::from_utf8(self.body.clone()).map_err(|err| BootError::Internal(err.to_string()))
276    }
277
278    pub fn body_json<T>(&self) -> Result<T>
279    where
280        T: DeserializeOwned,
281    {
282        if self.is_streaming() {
283            return Err(BootError::Internal(
284                "streaming response body cannot be read as JSON".to_string(),
285            ));
286        }
287        serde_json::from_slice(&self.body).map_err(|err| BootError::Internal(err.to_string()))
288    }
289
290    pub fn with_status(mut self, status: u16) -> Self {
291        self.status = status;
292        self
293    }
294
295    pub fn with_content_type(self, content_type: impl Into<String>) -> Self {
296        self.with_header("content-type", content_type)
297    }
298
299    pub fn with_content_length(self, content_length: u64) -> Self {
300        self.with_header("content-length", content_length.to_string())
301    }
302
303    pub fn with_location(self, location: impl Into<String>) -> Self {
304        self.with_header("location", location)
305    }
306
307    pub fn with_www_authenticate(self, challenge: impl Into<String>) -> Self {
308        self.with_header("www-authenticate", challenge)
309    }
310
311    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
312        self.headers
313            .insert(normalize_header_name(name), value.into());
314        self
315    }
316
317    pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
318        self.headers = normalize_headers(headers);
319        self
320    }
321
322    pub fn append_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
323        self.appended_headers
324            .push((normalize_header_name(name), value.into()));
325        self
326    }
327
328    pub fn append_www_authenticate(self, challenge: impl Into<String>) -> Self {
329        self.append_header("www-authenticate", challenge)
330    }
331
332    pub fn with_cookie(
333        self,
334        name: impl AsRef<str>,
335        value: impl AsRef<str>,
336        options: CookieOptions,
337    ) -> Result<Self> {
338        let header = options.set_cookie_header(name.as_ref(), value.as_ref())?;
339        Ok(self.append_header("set-cookie", header))
340    }
341
342    pub fn delete_cookie(self, name: impl AsRef<str>, options: CookieOptions) -> Result<Self> {
343        let header = options.delete_cookie_header(name.as_ref())?;
344        Ok(self.append_header("set-cookie", header))
345    }
346
347    pub fn is_streaming(&self) -> bool {
348        self.stream.is_some()
349    }
350
351    pub fn is_file_stream(&self) -> bool {
352        self.stream
353            .as_ref()
354            .is_some_and(|stream| stream.kind() == ResponseStreamKind::Body)
355    }
356
357    pub fn is_event_stream(&self) -> bool {
358        self.is_content_type("text/event-stream")
359    }
360
361    pub fn into_sse_stream(self) -> Option<SseStream> {
362        self.stream.and_then(|stream| stream.take())
363    }
364
365    pub fn into_body_stream(self) -> Option<StreamableFileStream> {
366        self.stream.and_then(|stream| stream.take_body())
367    }
368
369    pub fn header_entries(&self) -> impl Iterator<Item = (&str, &str)> {
370        self.headers
371            .iter()
372            .map(|(name, value)| (name.as_str(), value.as_str()))
373            .chain(
374                self.appended_headers
375                    .iter()
376                    .map(|(name, value)| (name.as_str(), value.as_str())),
377            )
378    }
379
380    pub fn validate_headers(&self) -> Result<()> {
381        for (name, value) in self.header_entries() {
382            validate_response_header(name, value)?;
383        }
384
385        Ok(())
386    }
387
388    pub fn header(&self, name: &str) -> Option<&str> {
389        get_header(&self.headers, name)
390    }
391
392    pub fn header_values(&self, name: &str) -> Vec<&str> {
393        let mut values = self.header(name).into_iter().collect::<Vec<_>>();
394        values.extend(
395            self.appended_headers
396                .iter()
397                .filter(|(key, _)| key.eq_ignore_ascii_case(name))
398                .map(|(_, value)| value.as_str()),
399        );
400        values
401    }
402
403    pub fn content_type(&self) -> Option<&str> {
404        self.header_values("content-type").into_iter().next()
405    }
406
407    pub fn location(&self) -> Option<&str> {
408        self.header_values("location").into_iter().next()
409    }
410
411    pub fn www_authenticate(&self) -> Option<&str> {
412        self.header_values("www-authenticate").into_iter().next()
413    }
414
415    pub fn www_authenticate_values(&self) -> Vec<&str> {
416        self.header_values("www-authenticate")
417    }
418
419    pub fn content_length(&self) -> Result<Option<u64>> {
420        let Some(content_length) = self.header_values("content-length").into_iter().next() else {
421            return Ok(None);
422        };
423
424        parse_content_length(content_length)
425            .map(Some)
426            .ok_or_else(|| {
427                BootError::Internal(format!("invalid content-length header: {content_length}"))
428            })
429    }
430
431    pub fn strict_content_length(&self) -> Result<Option<u64>> {
432        strict_content_length_values(
433            self.header_values("content-length"),
434            |content_length| {
435                BootError::Internal(format!(
436                    "invalid response content-length header: {content_length}"
437                ))
438            },
439            |expected_content_length, content_length| {
440                BootError::Internal(format!(
441                    "conflicting response content-length headers: {expected_content_length} != {content_length}"
442                ))
443            },
444        )
445    }
446
447    pub fn validate_content_length(&self) -> Result<()> {
448        let Some(content_length) = self.strict_content_length()? else {
449            return Ok(());
450        };
451        if self.is_streaming() {
452            if self.is_file_stream() {
453                return Ok(());
454            }
455            return Err(BootError::Internal(
456                "streaming responses must not include a content-length header".to_string(),
457            ));
458        }
459
460        let actual_body_length = self.body.len() as u64;
461        if actual_body_length == content_length {
462            return Ok(());
463        }
464
465        Err(BootError::Internal(format!(
466            "response content-length header does not match response body length: expected {content_length}, got {actual_body_length}"
467        )))
468    }
469
470    pub fn is_content_type(&self, media_type: &str) -> bool {
471        self.content_type()
472            .is_some_and(|content_type| matches_media_type(content_type, media_type))
473    }
474
475    pub fn is_json_content_type(&self) -> bool {
476        self.content_type().is_some_and(is_json_media_type)
477    }
478
479    pub fn has_body(&self) -> bool {
480        self.is_streaming() || !self.body.is_empty()
481    }
482
483    pub fn allows_body(&self) -> bool {
484        !(self.is_informational() || self.status == 204 || self.status == 304)
485    }
486
487    pub fn validate_body_allowed(&self) -> Result<()> {
488        if !self.has_body() || self.allows_body() {
489            return Ok(());
490        }
491
492        Err(BootError::Internal(format!(
493            "response status {} must not include a body",
494            self.status
495        )))
496    }
497
498    pub fn is_informational(&self) -> bool {
499        (100..200).contains(&self.status)
500    }
501
502    pub fn is_success(&self) -> bool {
503        (200..300).contains(&self.status)
504    }
505
506    pub fn is_redirection(&self) -> bool {
507        (300..400).contains(&self.status)
508    }
509
510    pub fn is_client_error(&self) -> bool {
511        (400..500).contains(&self.status)
512    }
513
514    pub fn is_server_error(&self) -> bool {
515        (500..600).contains(&self.status)
516    }
517
518    pub fn is_error(&self) -> bool {
519        self.is_client_error() || self.is_server_error()
520    }
521
522    pub fn is_valid_status(&self) -> bool {
523        (100..1000).contains(&self.status)
524    }
525
526    pub fn validate_status(&self) -> Result<()> {
527        if self.is_valid_status() {
528            return Ok(());
529        }
530
531        Err(BootError::Internal(format!(
532            "invalid response status {}",
533            self.status
534        )))
535    }
536
537    pub fn validate(&self) -> Result<()> {
538        self.validate_status()?;
539        self.validate_content_length()?;
540        self.validate_body_allowed()?;
541        self.validate_headers()
542    }
543
544    fn with_sse_stream<S>(mut self, stream: S) -> Self
545    where
546        S: Stream<Item = Result<SseEvent>> + Send + 'static,
547    {
548        self.stream = Some(SharedResponseStream::new(ResponseStream::Sse(Box::pin(
549            stream,
550        ))));
551        self
552    }
553
554    fn with_body_stream(mut self, stream: StreamableFileStream) -> Self {
555        self.stream = Some(SharedResponseStream::new(ResponseStream::Body(stream)));
556        self
557    }
558}
559
560fn http_error_name(status: u16) -> &'static str {
561    match status {
562        400 => "Bad Request",
563        401 => "Unauthorized",
564        402 => "Payment Required",
565        403 => "Forbidden",
566        404 => "Not Found",
567        405 => "Method Not Allowed",
568        406 => "Not Acceptable",
569        407 => "Proxy Authentication Required",
570        408 => "Request Timeout",
571        409 => "Conflict",
572        410 => "Gone",
573        411 => "Length Required",
574        412 => "Precondition Failed",
575        413 => "Payload Too Large",
576        414 => "URI Too Long",
577        415 => "Unsupported Media Type",
578        416 => "Range Not Satisfiable",
579        417 => "Expectation Failed",
580        418 => "I'm a teapot",
581        421 => "Misdirected Request",
582        422 => "Unprocessable Entity",
583        423 => "Locked",
584        424 => "Failed Dependency",
585        425 => "Too Early",
586        426 => "Upgrade Required",
587        428 => "Precondition Required",
588        429 => "Too Many Requests",
589        431 => "Request Header Fields Too Large",
590        451 => "Unavailable For Legal Reasons",
591        500 => "Internal Server Error",
592        501 => "Not Implemented",
593        502 => "Bad Gateway",
594        503 => "Service Unavailable",
595        504 => "Gateway Timeout",
596        505 => "HTTP Version Not Supported",
597        506 => "Variant Also Negotiates",
598        507 => "Insufficient Storage",
599        508 => "Loop Detected",
600        510 => "Not Extended",
601        511 => "Network Authentication Required",
602        _ => "Http Exception",
603    }
604}
605
606fn validate_response_header(name: &str, value: &str) -> Result<()> {
607    validate_header_name(name).map_err(|message| {
608        BootError::Internal(format!("invalid response header name {name:?}: {message}"))
609    })?;
610    validate_header_value(value).map_err(|message| {
611        BootError::Internal(format!(
612            "invalid response header value for {name:?}: {message}"
613        ))
614    })
615}