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#[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}