1use crate::error::Error;
5use bytes::Bytes;
6use http::{HeaderValue, StatusCode, header};
7use http_body_util::{BodyExt, Full, combinators::BoxBody};
8use serde::Serialize;
9
10#[derive(Debug)]
14pub struct BodyError(String);
15
16impl BodyError {
17 pub(crate) fn new(message: impl Into<String>) -> Self {
18 Self(message.into())
19 }
20}
21
22impl std::fmt::Display for BodyError {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.write_str(&self.0)
25 }
26}
27
28impl std::error::Error for BodyError {}
29
30impl From<std::convert::Infallible> for BodyError {
31 fn from(e: std::convert::Infallible) -> Self {
32 match e {}
33 }
34}
35
36pub struct JcBody(BoxBody<Bytes, BodyError>);
41
42impl JcBody {
43 pub fn full(bytes: impl Into<Bytes>) -> Self {
45 Self(Full::new(bytes.into()).map_err(BodyError::from).boxed())
46 }
47
48 pub fn empty() -> Self {
50 Self::full(Bytes::new())
51 }
52
53 pub fn stream<B>(body: B) -> Self
57 where
58 B: http_body::Body<Data = Bytes> + Send + Sync + 'static,
59 B::Error: Into<BodyError>,
60 {
61 Self(body.map_err(Into::into).boxed())
62 }
63}
64
65impl http_body::Body for JcBody {
66 type Data = Bytes;
67 type Error = BodyError;
68
69 fn poll_frame(
70 mut self: std::pin::Pin<&mut Self>,
71 cx: &mut std::task::Context<'_>,
72 ) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
73 std::pin::Pin::new(&mut self.0).poll_frame(cx)
74 }
75
76 fn is_end_stream(&self) -> bool {
77 self.0.is_end_stream()
78 }
79
80 fn size_hint(&self) -> http_body::SizeHint {
81 self.0.size_hint()
82 }
83}
84
85pub type Response = http::Response<JcBody>;
88
89pub trait IntoResponse {
91 fn into_response(self) -> Response;
92}
93
94pub struct Json<T>(pub T);
96
97pub struct Created<T>(pub T);
99
100pub struct NoContent;
102
103pub struct Redirect {
107 status: StatusCode,
108 location: String,
109}
110
111impl Redirect {
112 pub fn to(location: impl Into<String>) -> Self {
115 Self {
116 status: StatusCode::FOUND,
117 location: location.into(),
118 }
119 }
120
121 pub fn see_other(location: impl Into<String>) -> Self {
123 Self {
124 status: StatusCode::SEE_OTHER,
125 location: location.into(),
126 }
127 }
128
129 pub fn temporary(location: impl Into<String>) -> Self {
131 Self {
132 status: StatusCode::TEMPORARY_REDIRECT,
133 location: location.into(),
134 }
135 }
136
137 pub fn permanent(location: impl Into<String>) -> Self {
139 Self {
140 status: StatusCode::PERMANENT_REDIRECT,
141 location: location.into(),
142 }
143 }
144}
145
146impl IntoResponse for Redirect {
147 fn into_response(self) -> Response {
148 let value = match HeaderValue::from_str(&self.location) {
152 Ok(v) => v,
153 Err(_) => {
154 return Error::internal("redirect location is not a valid header value")
155 .into_response();
156 }
157 };
158 let mut r = http::Response::new(JcBody::empty());
159 *r.status_mut() = self.status;
160 r.headers_mut().insert(header::LOCATION, value);
161 r
162 }
163}
164
165fn full(status: StatusCode, content_type: &'static str, body: impl Into<Bytes>) -> Response {
166 let mut r = http::Response::new(JcBody::full(body));
167 *r.status_mut() = status;
168 r.headers_mut()
169 .insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
170 r
171}
172
173fn json_body<T: Serialize>(status: StatusCode, value: &T) -> Response {
174 match serde_json::to_vec(value) {
175 Ok(bytes) => full(status, "application/json", bytes),
176 Err(e) => Error::internal(format!("response serialization failed: {e}")).into_response(),
177 }
178}
179
180impl IntoResponse for Response {
181 fn into_response(self) -> Response {
182 self
183 }
184}
185
186impl IntoResponse for &'static str {
187 fn into_response(self) -> Response {
188 full(
189 StatusCode::OK,
190 "text/plain; charset=utf-8",
191 self.as_bytes().to_vec(),
192 )
193 }
194}
195
196impl IntoResponse for String {
197 fn into_response(self) -> Response {
198 full(
199 StatusCode::OK,
200 "text/plain; charset=utf-8",
201 self.into_bytes(),
202 )
203 }
204}
205
206impl IntoResponse for StatusCode {
207 fn into_response(self) -> Response {
208 let mut r = http::Response::new(JcBody::empty());
209 *r.status_mut() = self;
210 r
211 }
212}
213
214impl<T: Serialize> IntoResponse for Json<T> {
215 fn into_response(self) -> Response {
216 json_body(StatusCode::OK, &self.0)
217 }
218}
219
220impl<T: Serialize> IntoResponse for Created<T> {
221 fn into_response(self) -> Response {
222 json_body(StatusCode::CREATED, &self.0)
223 }
224}
225
226impl IntoResponse for NoContent {
227 fn into_response(self) -> Response {
228 let mut r = http::Response::new(JcBody::empty());
229 *r.status_mut() = StatusCode::NO_CONTENT;
230 r
231 }
232}
233
234impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
239 fn into_response(self) -> Response {
240 let (status, inner) = self;
241 let mut r = inner.into_response();
242 *r.status_mut() = status;
243 r
244 }
245}
246
247#[derive(Serialize)]
248struct ErrorBody<'a> {
249 code: &'a str,
250 message: &'a str,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 details: Option<&'a serde_json::Value>,
253}
254
255#[derive(Clone)]
261pub(crate) struct ErrorParts {
262 pub(crate) code: &'static str,
263 pub(crate) message: String,
264}
265
266impl IntoResponse for Error {
267 fn into_response(self) -> Response {
268 let mut r = json_body(
269 self.status(),
270 &ErrorBody {
271 code: self.code(),
272 message: self.message(),
273 details: self.details(),
274 },
275 );
276 r.extensions_mut().insert(ErrorParts {
279 code: self.code(),
280 message: self.message().to_string(),
281 });
282 r
283 }
284}
285
286impl<T: IntoResponse> IntoResponse for crate::Result<T> {
287 fn into_response(self) -> Response {
288 match self {
289 Ok(v) => v.into_response(),
290 Err(e) => e.into_response(),
291 }
292 }
293}
294
295pub struct StreamBody {
299 stream: std::pin::Pin<
300 Box<dyn futures_core::Stream<Item = Result<Bytes, Error>> + Send + Sync + 'static>,
301 >,
302 content_type: HeaderValue,
303 attachment: Option<HeaderValue>,
304 frame_timeout: std::time::Duration,
305}
306
307impl StreamBody {
308 pub const DEFAULT_FRAME_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
310
311 pub fn new<S>(stream: S) -> Self
314 where
315 S: futures_core::Stream<Item = Result<Bytes, Error>> + Send + Sync + 'static,
316 {
317 Self {
318 stream: Box::pin(stream),
319 content_type: HeaderValue::from_static("application/octet-stream"),
320 attachment: None,
321 frame_timeout: Self::DEFAULT_FRAME_TIMEOUT,
322 }
323 }
324
325 pub fn channel() -> (Self, BodySender) {
329 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, Error>>(16);
331 (Self::new(ReceiverStream(rx)), BodySender(tx))
332 }
333
334 pub fn content_type(mut self, value: &str) -> Self {
337 self.content_type =
338 HeaderValue::from_str(value).expect("content_type must be a valid header value");
339 self
340 }
341
342 pub fn attachment(mut self, filename: &str) -> Self {
348 let safe: String = filename
349 .chars()
350 .filter(|c| *c != '"' && *c != '\\' && !c.is_control())
351 .collect();
352 self.attachment = Some(
353 HeaderValue::from_str(&format!("attachment; filename=\"{safe}\""))
354 .expect("sanitized filename is a valid header value"),
355 );
356 self
357 }
358
359 pub fn frame_timeout(mut self, timeout: std::time::Duration) -> Self {
362 self.frame_timeout = timeout;
363 self
364 }
365}
366
367pub struct BodySender(tokio::sync::mpsc::Sender<Result<Bytes, Error>>);
369
370impl BodySender {
371 pub async fn send(&self, chunk: impl Into<Bytes>) -> bool {
373 self.0.send(Ok(chunk.into())).await.is_ok()
374 }
375 pub async fn fail(self, error: Error) -> bool {
378 self.0.send(Err(error)).await.is_ok()
379 }
380}
381
382struct ReceiverStream(tokio::sync::mpsc::Receiver<Result<Bytes, Error>>);
384
385impl futures_core::Stream for ReceiverStream {
386 type Item = Result<Bytes, Error>;
387 fn poll_next(
388 mut self: std::pin::Pin<&mut Self>,
389 cx: &mut std::task::Context<'_>,
390 ) -> std::task::Poll<Option<Self::Item>> {
391 self.0.poll_recv(cx)
392 }
393}
394
395struct TimedFrames {
399 stream: std::pin::Pin<
400 Box<dyn futures_core::Stream<Item = Result<Bytes, Error>> + Send + Sync + 'static>,
401 >,
402 timeout: std::time::Duration,
403 sleep: Option<std::pin::Pin<Box<tokio::time::Sleep>>>,
404}
405
406impl http_body::Body for TimedFrames {
407 type Data = Bytes;
408 type Error = BodyError;
409
410 fn poll_frame(
411 mut self: std::pin::Pin<&mut Self>,
412 cx: &mut std::task::Context<'_>,
413 ) -> std::task::Poll<Option<Result<http_body::Frame<Bytes>, BodyError>>> {
414 use std::future::Future;
415 use std::task::Poll;
416 match self.stream.as_mut().poll_next(cx) {
417 Poll::Ready(Some(Ok(chunk))) => {
418 self.sleep = None;
419 Poll::Ready(Some(Ok(http_body::Frame::data(chunk))))
420 }
421 Poll::Ready(Some(Err(e))) => {
422 self.sleep = None;
423 Poll::Ready(Some(Err(BodyError::new(format!(
424 "response stream failed: {e}"
425 )))))
426 }
427 Poll::Ready(None) => Poll::Ready(None),
428 Poll::Pending => {
429 let timeout = self.timeout;
430 let sleep = self
431 .sleep
432 .get_or_insert_with(|| Box::pin(tokio::time::sleep(timeout)));
433 match sleep.as_mut().poll(cx) {
434 Poll::Ready(()) => {
435 self.sleep = None;
436 Poll::Ready(Some(Err(BodyError::new(
437 "response stream timed out producing the next chunk",
438 ))))
439 }
440 Poll::Pending => Poll::Pending,
441 }
442 }
443 }
444 }
445}
446
447impl IntoResponse for StreamBody {
448 fn into_response(self) -> Response {
449 let body = JcBody::stream(TimedFrames {
450 stream: self.stream,
451 timeout: self.frame_timeout,
452 sleep: None,
453 });
454 let mut r = http::Response::new(body);
455 r.headers_mut()
456 .insert(header::CONTENT_TYPE, self.content_type);
457 if let Some(disposition) = self.attachment {
458 r.headers_mut()
459 .insert(header::CONTENT_DISPOSITION, disposition);
460 }
461 r
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 fn body_of(r: Response) -> String {
470 let collected = futures_executor_lite(r.into_body());
471 String::from_utf8(collected.to_vec()).unwrap()
472 }
473
474 fn futures_executor_lite(body: JcBody) -> Bytes {
478 let fut = body.collect();
479 let mut fut = Box::pin(fut);
480 let waker = std::task::Waker::noop();
481 let mut cx = std::task::Context::from_waker(waker);
482 match fut.as_mut().poll(&mut cx) {
483 std::task::Poll::Ready(Ok(c)) => c.to_bytes(),
484 _ => panic!("buffered body was not immediately ready"),
485 }
486 }
487
488 #[test]
489 fn str_becomes_200_text() {
490 let r = "hello".into_response();
491 assert_eq!(r.status(), StatusCode::OK);
492 assert_eq!(
493 r.headers()[header::CONTENT_TYPE],
494 "text/plain; charset=utf-8"
495 );
496 assert_eq!(body_of(r), "hello");
497 }
498
499 #[test]
500 fn json_wrapper_sets_content_type() {
501 #[derive(Serialize)]
502 struct Todo {
503 id: u32,
504 }
505 let r = Json(Todo { id: 7 }).into_response();
506 assert_eq!(r.status(), StatusCode::OK);
507 assert_eq!(r.headers()[header::CONTENT_TYPE], "application/json");
508 assert_eq!(body_of(r), r#"{"id":7}"#);
509 }
510
511 #[test]
512 fn created_is_201_and_no_content_is_204() {
513 #[derive(Serialize)]
514 struct T {
515 ok: bool,
516 }
517 assert_eq!(
518 Created(T { ok: true }).into_response().status(),
519 StatusCode::CREATED
520 );
521 let r = NoContent.into_response();
522 assert_eq!(r.status(), StatusCode::NO_CONTENT);
523 assert_eq!(body_of(r), "");
524 }
525
526 #[test]
527 fn errors_render_code_and_message_json() {
528 let r = Error::not_found().into_response();
529 assert_eq!(r.status(), StatusCode::NOT_FOUND);
530 assert_eq!(body_of(r), r#"{"code":"JC0404","message":"not found"}"#);
531 }
532
533 #[test]
534 fn error_details_appear_in_the_body_only_when_present() {
535 let r = Error::not_found().into_response();
536 assert_eq!(body_of(r), r#"{"code":"JC0404","message":"not found"}"#);
537 let r = Error::unprocessable("validation failed")
538 .with_details(serde_json::json!([{ "field": "t" }]))
539 .into_response();
540 assert_eq!(
541 body_of(r),
542 r#"{"code":"JC0422","message":"validation failed","details":[{"field":"t"}]}"#
543 );
544 }
545
546 #[test]
547 fn result_renders_ok_or_err() {
548 let ok: crate::Result<&'static str> = Ok("fine");
549 assert_eq!(ok.into_response().status(), StatusCode::OK);
550 let err: crate::Result<&'static str> = Err(Error::bad_request("x"));
551 assert_eq!(err.into_response().status(), StatusCode::BAD_REQUEST);
552 }
553
554 #[test]
555 fn redirect_to_is_302_with_location_and_empty_body() {
556 let r = Redirect::to("/x").into_response();
557 assert_eq!(r.status(), StatusCode::FOUND);
558 assert_eq!(r.headers()[header::LOCATION], "/x");
559 assert_eq!(body_of(r), "");
560 }
561
562 #[test]
563 fn redirect_constructors_set_their_status_and_location() {
564 for (build, status) in [
567 (Redirect::see_other("/a") as Redirect, StatusCode::SEE_OTHER),
568 (Redirect::temporary("/b"), StatusCode::TEMPORARY_REDIRECT),
569 (Redirect::permanent("/c"), StatusCode::PERMANENT_REDIRECT),
570 ] {
571 let r = build.into_response();
572 assert_eq!(r.status(), status);
573 assert!(r.headers().contains_key(header::LOCATION));
574 }
575 }
576
577 #[test]
578 fn redirect_with_invalid_location_is_a_non_panicking_500() {
579 let r = Redirect::to("/bad\nlocation").into_response();
582 assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR);
583 assert!(r.headers().get(header::LOCATION).is_none());
584 }
585
586 #[test]
587 fn status_tuple_overrides_status_keeping_the_json_body() {
588 #[derive(Serialize)]
591 struct Summary {
592 queued: u32,
593 }
594 let r = (StatusCode::ACCEPTED, Json(Summary { queued: 3 })).into_response();
595 assert_eq!(r.status(), StatusCode::ACCEPTED);
596 assert_eq!(r.headers()[header::CONTENT_TYPE], "application/json");
597 assert_eq!(body_of(r), r#"{"queued":3}"#);
598 }
599
600 #[test]
601 fn status_tuple_overrides_status_keeping_the_text_body() {
602 let r = (StatusCode::ACCEPTED, "queued").into_response();
603 assert_eq!(r.status(), StatusCode::ACCEPTED);
604 assert_eq!(
605 r.headers()[header::CONTENT_TYPE],
606 "text/plain; charset=utf-8"
607 );
608 assert_eq!(body_of(r), "queued");
609 }
610
611 #[tokio::test]
612 async fn boxed_bodies_stream_and_collect() {
613 struct Chunks(std::collections::VecDeque<Bytes>);
615 impl http_body::Body for Chunks {
616 type Data = Bytes;
617 type Error = std::convert::Infallible;
618 fn poll_frame(
619 mut self: std::pin::Pin<&mut Self>,
620 _cx: &mut std::task::Context<'_>,
621 ) -> std::task::Poll<Option<Result<http_body::Frame<Bytes>, Self::Error>>> {
622 std::task::Poll::Ready(self.0.pop_front().map(|b| Ok(http_body::Frame::data(b))))
623 }
624 }
625 let body = JcBody::stream(Chunks(
626 [Bytes::from("ab"), Bytes::from("cd")].into_iter().collect(),
627 ));
628 use http_body_util::BodyExt;
629 let collected = body.collect().await.unwrap().to_bytes();
630 assert_eq!(collected, Bytes::from("abcd"));
631 }
632
633 #[tokio::test]
634 async fn stream_body_streams_with_content_type_and_disposition() {
635 let (body, tx) = StreamBody::channel();
636 let send = async move {
637 assert!(tx.send("a,b\n").await);
638 assert!(tx.send("1,2\n").await);
639 };
640 let r = body
641 .content_type("text/csv")
642 .attachment("export.csv")
643 .into_response();
644 assert_eq!(r.status(), StatusCode::OK);
645 assert_eq!(r.headers()[header::CONTENT_TYPE], "text/csv");
646 assert_eq!(
647 r.headers()[header::CONTENT_DISPOSITION],
648 "attachment; filename=\"export.csv\""
649 );
650 let (_, collected) = tokio::join!(send, r.into_body().collect());
651 assert_eq!(collected.unwrap().to_bytes(), Bytes::from("a,b\n1,2\n"));
652 }
653
654 #[tokio::test(start_paused = true)]
655 async fn stream_body_frame_timeout_errors_the_body() {
656 struct Never;
657 impl futures_core::Stream for Never {
658 type Item = Result<Bytes, Error>;
659 fn poll_next(
660 self: std::pin::Pin<&mut Self>,
661 _cx: &mut std::task::Context<'_>,
662 ) -> std::task::Poll<Option<Self::Item>> {
663 std::task::Poll::Pending
664 }
665 }
666 let body = StreamBody::new(Never)
667 .frame_timeout(std::time::Duration::from_millis(100))
668 .into_response()
669 .into_body();
670 use http_body_util::BodyExt;
671 let err = body
672 .collect()
673 .await
674 .expect_err("stall must error, not end cleanly");
675 assert!(err.to_string().contains("timed out"), "{err}");
676 }
677
678 #[tokio::test]
679 async fn channel_fail_surfaces_as_a_body_error_carrying_the_message() {
680 let (body, tx) = StreamBody::channel();
685 let produce = async move {
686 assert!(tx.send("first chunk").await, "client present");
687 assert!(tx.fail(Error::internal("boom")).await, "fail delivered");
688 };
689 let response = body.into_response();
690 use http_body_util::BodyExt;
691 let (_, collected) = tokio::join!(produce, response.into_body().collect());
692 let err = collected.expect_err("a failed producer must error the body, not end cleanly");
693 assert!(
694 err.to_string().contains("boom"),
695 "the propagated message must survive to the body error: {err}"
696 );
697 }
698
699 #[tokio::test]
700 async fn stream_body_composes_through_a_real_handler_dispatch() {
701 use crate::prelude::*;
702 async fn export() -> Result<StreamBody> {
703 let (body, tx) = StreamBody::channel();
704 tokio::spawn(async move {
705 tx.send("id,name\n").await;
706 tx.send("1,ada\n").await;
707 });
708 Ok(body.content_type("text/csv"))
709 }
710 let t = App::new().route("/export", get(export)).into_test();
711 let r = t.get("/export").await;
712 assert_eq!(r.status(), StatusCode::OK);
713 assert_eq!(r.headers()[header::CONTENT_TYPE], "text/csv");
714 assert_eq!(r.text(), "id,name\n1,ada\n");
715 }
716}