Skip to main content

tachyon_web/http/
response.rs

1//! Response utilities and the `IntoResponse` trait.
2
3/// Server-Sent Events (SSE) — see the module docs for usage. Requires the `sse` feature.
4#[cfg(feature = "sse")]
5pub mod sse;
6#[cfg(feature = "sse")]
7pub use sse::Sse;
8
9use bytes::Bytes;
10use http_body_util::BodyExt;
11use http_body_util::Full;
12use http_body_util::combinators::UnsyncBoxBody as BoxBody;
13use hyper::body::{Body as HyperBody, Frame, SizeHint};
14use hyper::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
15use hyper::{Response, StatusCode};
16#[cfg(feature = "json")]
17use serde::Serialize;
18use std::pin::Pin;
19use std::task::{Context, Poll};
20
21/// The unified body type for Tachyon-Web responses.
22#[derive(Default)]
23pub enum Body {
24    /// A single full chunk of bytes in memory.
25    Full(Full<Bytes>),
26    /// An empty body.
27    #[default]
28    Empty,
29    /// A boxed stream body for streaming data (like SSE or large files).
30    Stream(BoxBody<Bytes, crate::http::error::Error>),
31}
32
33impl std::fmt::Debug for Body {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::Full(full) => f.debug_tuple("Full").field(full).finish(),
37            Self::Empty => f.write_str("Empty"),
38            Self::Stream(_) => f.debug_tuple("Stream").field(&"<stream>").finish(),
39        }
40    }
41}
42
43impl Body {
44    /// Create a new empty body.
45    #[must_use]
46    pub const fn empty() -> Self {
47        Self::Empty
48    }
49
50    /// Create a new body from a single chunk of bytes.
51    pub fn full(bytes: Bytes) -> Self {
52        Self::Full(Full::new(bytes))
53    }
54
55    /// Create a streaming body from an implementation.
56    pub fn stream<B>(body: B) -> Self
57    where
58        B: HyperBody<Data = Bytes> + Send + 'static,
59        B::Error: Into<crate::http::error::Error>,
60    {
61        Self::Stream(BoxBody::new(body.map_err(std::convert::Into::into)))
62    }
63
64    /// Buffers the entire body into memory, rejecting bodies larger than `limit`
65    /// bytes with a `413 Payload Too Large` rejection instead of allocating
66    /// unbounded memory.
67    ///
68    /// Works uniformly across all three variants — for `Full`/`Empty` this
69    /// resolves immediately with no I/O; for `Stream` it awaits incoming frames.
70    ///
71    /// # Errors
72    /// Returns a `413` rejection if the body exceeds `limit`, or a `400` rejection
73    /// if reading the body otherwise fails (e.g. a malformed chunked transfer).
74    pub async fn collect_bytes(self, limit: usize) -> Result<Bytes, crate::http::error::Error> {
75        match http_body_util::Limited::new(self, limit).collect().await {
76            Ok(collected) => Ok(collected.to_bytes()),
77            Err(e) => {
78                if e.downcast_ref::<http_body_util::LengthLimitError>()
79                    .is_some()
80                {
81                    Err(crate::http::error::Error::Rejection {
82                        status: StatusCode::PAYLOAD_TOO_LARGE,
83                        message: "Request body exceeds the maximum allowed size".to_string(),
84                    })
85                } else {
86                    Err(crate::http::error::Error::Rejection {
87                        status: StatusCode::BAD_REQUEST,
88                        message: format!("Failed to read request body: {e}"),
89                    })
90                }
91            }
92        }
93    }
94}
95
96impl HyperBody for Body {
97    type Data = Bytes;
98    type Error = crate::http::error::Error;
99
100    fn poll_frame(
101        self: Pin<&mut Self>,
102        cx: &mut Context<'_>,
103    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
104        match self.get_mut() {
105            Self::Full(full) => match Pin::new(full).poll_frame(cx) {
106                Poll::Ready(Some(Ok(frame))) => Poll::Ready(Some(Ok(frame))),
107                Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(
108                    crate::http::error::Error::Internal(e.to_string()),
109                ))),
110                Poll::Ready(None) => Poll::Ready(None),
111                Poll::Pending => Poll::Pending,
112            },
113            Self::Empty => Poll::Ready(None),
114            Self::Stream(stream) => Pin::new(stream).poll_frame(cx),
115        }
116    }
117
118    fn is_end_stream(&self) -> bool {
119        match self {
120            Self::Full(full) => full.is_end_stream(),
121            Self::Empty => true,
122            Self::Stream(stream) => stream.is_end_stream(),
123        }
124    }
125
126    fn size_hint(&self) -> SizeHint {
127        match self {
128            Self::Full(full) => full.size_hint(),
129            Self::Empty => SizeHint::with_exact(0),
130            Self::Stream(stream) => stream.size_hint(),
131        }
132    }
133}
134
135/// Trait for generating an HTTP response.
136pub trait IntoResponse {
137    /// Convert the type into a `Response<Body>`.
138    fn into_response(self) -> Response<Body>;
139}
140
141impl IntoResponse for Response<Body> {
142    fn into_response(self) -> Self {
143        self
144    }
145}
146
147impl IntoResponse for Response<Full<Bytes>> {
148    fn into_response(self) -> Response<Body> {
149        let (parts, body) = self.into_parts();
150        Response::from_parts(parts, Body::Full(body))
151    }
152}
153
154impl IntoResponse for StatusCode {
155    fn into_response(self) -> Response<Body> {
156        let mut res = Response::new(Body::empty());
157        *res.status_mut() = self;
158        res
159    }
160}
161
162impl IntoResponse for String {
163    fn into_response(self) -> Response<Body> {
164        let mut res = Response::new(Body::full(Bytes::from(self)));
165        let _ = res.headers_mut().insert(
166            CONTENT_TYPE,
167            HeaderValue::from_static("text/plain; charset=utf-8"),
168        );
169        res
170    }
171}
172
173impl IntoResponse for &'static str {
174    fn into_response(self) -> Response<Body> {
175        let mut res = Response::new(Body::full(Bytes::from_static(self.as_bytes())));
176        let _ = res.headers_mut().insert(
177            CONTENT_TYPE,
178            HeaderValue::from_static("text/plain; charset=utf-8"),
179        );
180        res
181    }
182}
183
184impl IntoResponse for Vec<u8> {
185    fn into_response(self) -> Response<Body> {
186        let mut res = Response::new(Body::full(Bytes::from(self)));
187        let _ = res.headers_mut().insert(
188            CONTENT_TYPE,
189            HeaderValue::from_static("application/octet-stream"),
190        );
191        res
192    }
193}
194
195impl IntoResponse for &'static [u8] {
196    fn into_response(self) -> Response<Body> {
197        let mut res = Response::new(Body::full(Bytes::from_static(self)));
198        let _ = res.headers_mut().insert(
199            CONTENT_TYPE,
200            HeaderValue::from_static("application/octet-stream"),
201        );
202        res
203    }
204}
205
206/// An HTML response.
207#[derive(Debug, Clone)]
208pub struct Html<T>(pub T);
209
210impl<T> IntoResponse for Html<T>
211where
212    T: Into<Bytes>,
213{
214    fn into_response(self) -> Response<Body> {
215        let mut res = Response::new(Body::full(self.0.into()));
216        let _ = res.headers_mut().insert(
217            CONTENT_TYPE,
218            HeaderValue::from_static("text/html; charset=utf-8"),
219        );
220        res
221    }
222}
223
224#[cfg(feature = "json")]
225thread_local! {
226    static JSON_WRITE_BUF: std::cell::RefCell<bytes::BytesMut> =
227        std::cell::RefCell::new(bytes::BytesMut::with_capacity(1024));
228}
229
230/// Adapts a `&mut BytesMut` to `std::io::Write` for `serde_json::to_writer` —
231/// `bytes` only implements `fmt::Write` for `BytesMut`, not `io::Write`.
232#[cfg(feature = "json")]
233struct BytesMutWriter<'a>(&'a mut bytes::BytesMut);
234
235#[cfg(feature = "json")]
236impl std::io::Write for BytesMutWriter<'_> {
237    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
238        self.0.extend_from_slice(buf);
239        Ok(buf.len())
240    }
241
242    fn flush(&mut self) -> std::io::Result<()> {
243        Ok(())
244    }
245}
246
247/// A JSON response. Requires the `json` feature.
248#[cfg(feature = "json")]
249pub use crate::routing::extract::Json;
250
251#[cfg(feature = "json")]
252impl<T> IntoResponse for Json<T>
253where
254    T: Serialize,
255{
256    fn into_response(self) -> Response<Body> {
257        // `try_borrow_mut` (rather than `borrow_mut`) so that a `Serialize` impl
258        // which re-enters this function on the same thread (e.g. by serializing a
259        // nested `Json<_>` internally) falls back to a fresh, non-shared buffer
260        // instead of panicking on an already-borrowed `RefCell` — a panic path the
261        // crate's `deny(clippy::panic/unwrap_used/expect_used)` lints can't catch
262        // since it originates inside `RefCell` itself, not an explicit unwrap.
263        #[allow(clippy::single_match_else)]
264        let result = JSON_WRITE_BUF.with(|buf| match buf.try_borrow_mut() {
265            Ok(mut b) => {
266                // `b` is always empty on entry (every exit path below leaves it that
267                // way), so the written frame is exactly `[0, b.len())`.
268                let res = match serde_json::to_writer(BytesMutWriter(&mut b), &self.0) {
269                    // `split_to` hands the written bytes to the caller as a `Bytes`
270                    // that shares the same underlying allocation — no memcpy — while
271                    // `b` keeps the (empty) view over its remaining spare tail
272                    // capacity, ready to be written into again next call with no
273                    // fresh allocation in the common case.
274                    Ok(()) => {
275                        let len = b.len();
276                        Ok(b.split_to(len).freeze())
277                    }
278                    Err(err) => {
279                        // Discard whatever partial bytes a failed serialize left behind.
280                        b.clear();
281                        Err(err)
282                    }
283                };
284                // A single oversized payload shouldn't permanently inflate this
285                // thread's buffer — same cap the old `Vec`-based version enforced.
286                if b.capacity() > 65536 {
287                    *b = bytes::BytesMut::with_capacity(1024);
288                }
289                res
290            }
291            Err(_) => {
292                let mut b = Vec::with_capacity(1024);
293                match serde_json::to_writer(&mut b, &self.0) {
294                    Ok(()) => Ok(Bytes::from(b)),
295                    Err(err) => Err(err),
296                }
297            }
298        });
299
300        match result {
301            Ok(bytes) => {
302                let mut res = Response::new(Body::full(bytes));
303                let _ = res
304                    .headers_mut()
305                    .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
306                res
307            }
308            Err(err) => {
309                let mut res = Response::new(Body::full(Bytes::from(format!(
310                    "Failed to serialize JSON: {err}"
311                ))));
312                *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
313                let _ = res.headers_mut().insert(
314                    CONTENT_TYPE,
315                    HeaderValue::from_static("text/plain; charset=utf-8"),
316                );
317                res
318            }
319        }
320    }
321}
322
323impl<R> IntoResponse for (StatusCode, R)
324where
325    R: IntoResponse,
326{
327    fn into_response(self) -> Response<Body> {
328        let (status, res) = self;
329        let mut response = res.into_response();
330        *response.status_mut() = status;
331        response
332    }
333}
334
335// ─── IntoResponseParts ─────────────────────────────────────────────────────────
336//
337// Generalizes response-tuple composition beyond a fixed whitelist of shapes.
338// Any type implementing `IntoResponseParts` (headers, cookies, extensions, or a
339// user's own typed-header wrapper) can be combined — in any order, up to 8 of
340// them — with an optional leading `StatusCode` and a trailing body, mirroring
341// Axum's `IntoResponseParts` design. e.g. `(HeaderMap, Cookies, Json<T>)` and
342// `(StatusCode, Cookies, HeaderMap, Json<T>)` both just work.
343
344/// The response under construction, passed to [`IntoResponseParts::into_response_parts`]
345/// so implementors can attach headers/extensions without unpacking the whole response.
346#[derive(Debug)]
347pub struct ResponseParts {
348    res: Response<Body>,
349}
350
351impl ResponseParts {
352    /// Mutable access to the headers of the response being built.
353    pub fn headers_mut(&mut self) -> &mut HeaderMap {
354        self.res.headers_mut()
355    }
356
357    /// Mutable access to the extensions of the response being built.
358    pub fn extensions_mut(&mut self) -> &mut hyper::http::Extensions {
359        self.res.extensions_mut()
360    }
361}
362
363/// Trait for types that attach headers/extensions to a response without providing
364/// its body — the building block for flexible, order-independent response tuples.
365///
366/// Implement this for your own typed-header wrappers to use them anywhere in a
367/// response tuple, e.g. `(MyCacheControl, StatusCode, Json<T>)`.
368pub trait IntoResponseParts {
369    /// The rejection response returned if attaching the parts fails.
370    type Error: IntoResponse;
371
372    /// Attach `self` onto `res`, returning the updated parts (or a rejection).
373    ///
374    /// # Errors
375    /// Returns `Self::Error` if the parts cannot be attached (e.g. an invalid header value).
376    fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error>;
377}
378
379impl IntoResponseParts for HeaderMap {
380    type Error = std::convert::Infallible;
381
382    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
383        res.headers_mut().extend(self);
384        Ok(res)
385    }
386}
387
388impl<T> IntoResponseParts for Option<T>
389where
390    T: IntoResponseParts,
391{
392    type Error = T::Error;
393
394    fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
395        match self {
396            Some(parts) => parts.into_response_parts(res),
397            None => Ok(res),
398        }
399    }
400}
401
402impl<T> IntoResponseParts for crate::routing::extract::Extension<T>
403where
404    T: Clone + Send + Sync + 'static,
405{
406    type Error = std::convert::Infallible;
407
408    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
409        res.extensions_mut().insert(self.0);
410        Ok(res)
411    }
412}
413
414/// Appends an arbitrary collection of headers to a response without
415/// replacing any existing header of the same name, matching
416/// `axum::response::AppendHeaders`.
417///
418/// A bare `HeaderMap` already appends (see the `IntoResponseParts` impl
419/// above) — this exists for the common case of a small, fixed list of
420/// `(name, value)` pairs (e.g. `[("x-custom", "1"), ("x-other", "2")]`)
421/// without constructing a full `HeaderMap` first.
422#[derive(Debug, Clone, Copy)]
423pub struct AppendHeaders<I>(pub I);
424
425impl<I, K, V> IntoResponseParts for AppendHeaders<I>
426where
427    I: IntoIterator<Item = (K, V)>,
428    K: TryInto<HeaderName>,
429    V: TryInto<HeaderValue>,
430{
431    type Error = crate::http::error::Error;
432
433    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
434        for (key, value) in self.0 {
435            let key = key
436                .try_into()
437                .map_err(|_| crate::http::error::Error::Rejection {
438                    status: StatusCode::INTERNAL_SERVER_ERROR,
439                    message: "AppendHeaders: invalid header name".to_string(),
440                })?;
441            let value = value
442                .try_into()
443                .map_err(|_| crate::http::error::Error::Rejection {
444                    status: StatusCode::INTERNAL_SERVER_ERROR,
445                    message: "AppendHeaders: invalid header value".to_string(),
446                })?;
447            res.headers_mut().append(key, value);
448        }
449        Ok(res)
450    }
451}
452
453#[cfg(feature = "cookies")]
454impl IntoResponseParts for crate::routing::extract::Cookies {
455    type Error = std::convert::Infallible;
456
457    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
458        for cookie in self.jar.delta() {
459            if let Ok(header_val) = HeaderValue::try_from(cookie.encoded().to_string()) {
460                res.headers_mut()
461                    .append(hyper::header::SET_COOKIE, header_val);
462            }
463        }
464        Ok(res)
465    }
466}
467
468macro_rules! impl_into_response_for_parts_tuples {
469    ($($T:ident),+) => {
470        impl<R, $($T),+> IntoResponse for ($($T,)+ R)
471        where
472            R: IntoResponse,
473            $( $T: IntoResponseParts, )+
474        {
475            fn into_response(self) -> Response<Body> {
476                #[allow(non_snake_case)]
477                let ($($T,)+ res) = self;
478                let mut parts = ResponseParts { res: res.into_response() };
479                $(
480                    parts = match $T.into_response_parts(parts) {
481                        Ok(p) => p,
482                        Err(rejection) => return rejection.into_response(),
483                    };
484                )+
485                parts.res
486            }
487        }
488
489        impl<R, $($T),+> IntoResponse for (StatusCode, $($T,)+ R)
490        where
491            R: IntoResponse,
492            $( $T: IntoResponseParts, )+
493        {
494            fn into_response(self) -> Response<Body> {
495                #[allow(non_snake_case)]
496                let (status, $($T,)+ res) = self;
497                let mut response = <($($T,)+ R) as IntoResponse>::into_response(($($T,)+ res));
498                *response.status_mut() = status;
499                response
500            }
501        }
502    };
503}
504
505impl_into_response_for_parts_tuples!(T1);
506impl_into_response_for_parts_tuples!(T1, T2);
507impl_into_response_for_parts_tuples!(T1, T2, T3);
508impl_into_response_for_parts_tuples!(T1, T2, T3, T4);
509impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5);
510impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5, T6);
511impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5, T6, T7);
512impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5, T6, T7, T8);
513
514impl IntoResponse for () {
515    fn into_response(self) -> Response<Body> {
516        Response::builder()
517            .status(StatusCode::OK)
518            .body(Body::empty())
519            .unwrap_or_else(|_| Response::new(Body::empty()))
520    }
521}
522
523impl<T, E> IntoResponse for Result<T, E>
524where
525    T: IntoResponse,
526    E: IntoResponse,
527{
528    fn into_response(self) -> Response<Body> {
529        match self {
530            Ok(value) => value.into_response(),
531            Err(err) => err.into_response(),
532        }
533    }
534}
535
536impl IntoResponse for std::convert::Infallible {
537    fn into_response(self) -> Response<Body> {
538        match self {}
539    }
540}
541
542/// Response that redirects the client to another location.
543#[derive(Debug, Clone)]
544pub struct Redirect {
545    status_code: StatusCode,
546    location: HeaderValue,
547}
548
549impl Redirect {
550    /// Create a `303 See Other` redirect to the given URI.
551    ///
552    /// If `uri` contains bytes that aren't valid in an HTTP header value (e.g. a
553    /// stray `\n` or `\r`), this crate's `deny(clippy::panic)` policy rules out
554    /// panicking the way Axum's equivalent does — instead the `Location` header
555    /// silently falls back to `/`. Validate/sanitize `uri` yourself if it's ever
556    /// built from user-controlled or templated data, since a silent fallback to
557    /// `/` is easy to miss.
558    #[must_use]
559    pub fn to(uri: &str) -> Self {
560        Self {
561            status_code: StatusCode::SEE_OTHER,
562            location: HeaderValue::try_from(uri).unwrap_or_else(|_| HeaderValue::from_static("/")),
563        }
564    }
565
566    /// Create a `307 Temporary Redirect` redirect to the given URI.
567    ///
568    /// See [`Redirect::to`]'s docs for the fallback behavior on an invalid `uri`.
569    #[must_use]
570    pub fn temporary(uri: &str) -> Self {
571        Self {
572            status_code: StatusCode::TEMPORARY_REDIRECT,
573            location: HeaderValue::try_from(uri).unwrap_or_else(|_| HeaderValue::from_static("/")),
574        }
575    }
576
577    /// Create a `308 Permanent Redirect` redirect to the given URI.
578    ///
579    /// See [`Redirect::to`]'s docs for the fallback behavior on an invalid `uri`.
580    #[must_use]
581    pub fn permanent(uri: &str) -> Self {
582        Self {
583            status_code: StatusCode::PERMANENT_REDIRECT,
584            location: HeaderValue::try_from(uri).unwrap_or_else(|_| HeaderValue::from_static("/")),
585        }
586    }
587}
588
589impl IntoResponse for Redirect {
590    fn into_response(self) -> Response<Body> {
591        let mut resp = Response::new(Body::empty());
592        *resp.status_mut() = self.status_code;
593        let _ = resp
594            .headers_mut()
595            .insert(hyper::header::LOCATION, self.location);
596        resp
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use crate::routing::extract::Cookies;
604    use hyper::HeaderMap;
605
606    #[test]
607    fn test_body_debug_and_size_hint() {
608        let b1 = Body::empty();
609        assert!(format!("{b1:?}").contains("Empty"));
610        assert!(b1.is_end_stream());
611        assert_eq!(b1.size_hint().exact(), Some(0));
612
613        let b2 = Body::full(Bytes::from("test"));
614        assert!(format!("{b2:?}").contains("Full"));
615        assert!(!b2.is_end_stream());
616        assert_eq!(b2.size_hint().exact(), Some(4));
617
618        let stream_body = BoxBody::new(
619            http_body_util::Empty::<Bytes>::new()
620                .map_err(|e| crate::http::error::Error::Internal(e.to_string())),
621        );
622        let b3 = Body::Stream(stream_body);
623        assert!(format!("{b3:?}").contains("Stream"));
624        assert!(b3.is_end_stream());
625        assert_eq!(b3.size_hint().exact(), Some(0));
626    }
627
628    #[tokio::test]
629    async fn test_body_poll_frame() {
630        use hyper::body::Body as _;
631        let mut b1 = Body::full(Bytes::from("a"));
632        let mut b1_pin = Pin::new(&mut b1);
633        let cx = &mut Context::from_waker(futures::task::noop_waker_ref());
634        let f1 = b1_pin.as_mut().poll_frame(cx);
635        assert!(matches!(f1, Poll::Ready(Some(Ok(_)))));
636        let f2 = b1_pin.as_mut().poll_frame(cx);
637        assert!(matches!(f2, Poll::Ready(None)));
638
639        let mut b2 = Body::empty();
640        let f3 = Pin::new(&mut b2).poll_frame(cx);
641        assert!(matches!(f3, Poll::Ready(None)));
642    }
643
644    #[cfg(feature = "json")]
645    struct FailSerialize;
646    #[cfg(feature = "json")]
647    impl Serialize for FailSerialize {
648        fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
649        where
650            S: serde::Serializer,
651        {
652            Err(serde::ser::Error::custom("failed"))
653        }
654    }
655
656    #[test]
657    fn test_into_response_implementations() {
658        let full_resp = Response::new(Full::new(Bytes::from("abc")));
659        let r1 = full_resp.into_response();
660        assert_eq!(r1.status(), StatusCode::OK);
661
662        let v: Vec<u8> = vec![1, 2, 3];
663        let r2 = v.into_response();
664        assert_eq!(
665            r2.headers().get(CONTENT_TYPE).unwrap(),
666            "application/octet-stream"
667        );
668
669        let s: &'static [u8] = b"static";
670        let r3 = s.into_response();
671        assert_eq!(
672            r3.headers().get(CONTENT_TYPE).unwrap(),
673            "application/octet-stream"
674        );
675
676        #[cfg(feature = "json")]
677        {
678            let fail_json = Json(FailSerialize);
679            let r4 = fail_json.into_response();
680            assert_eq!(r4.status(), StatusCode::INTERNAL_SERVER_ERROR);
681        }
682
683        let mut h = HeaderMap::new();
684        let _ = h.insert("x-custom", HeaderValue::from_static("val"));
685        let r5 = (h.clone(), "body").into_response();
686        assert_eq!(r5.headers().get("x-custom").unwrap(), "val");
687
688        let r6 = (StatusCode::CREATED, h.clone(), "body").into_response();
689        assert_eq!(r6.status(), StatusCode::CREATED);
690        assert_eq!(r6.headers().get("x-custom").unwrap(), "val");
691
692        let cookies = Cookies::new();
693        let r7 = (StatusCode::ACCEPTED, cookies, "body").into_response();
694        assert_eq!(r7.status(), StatusCode::ACCEPTED);
695
696        let r8 = ().into_response();
697        assert_eq!(r8.status(), StatusCode::OK);
698
699        let res_ok: Result<&str, &str> = Ok("ok");
700        let r9 = res_ok.into_response();
701        assert_eq!(r9.status(), StatusCode::OK);
702
703        let res_err: Result<&str, &str> = Err("err");
704        let r10 = res_err.into_response();
705        assert_eq!(r10.status(), StatusCode::OK);
706    }
707
708    #[test]
709    fn option_into_response_parts_some_and_none() {
710        let mut h = HeaderMap::new();
711        let _ = h.insert("x-opt", HeaderValue::from_static("present"));
712
713        let with_some = (Some(h), "body").into_response();
714        assert_eq!(with_some.headers().get("x-opt").unwrap(), "present");
715
716        let with_none = (None::<HeaderMap>, "body").into_response();
717        assert!(with_none.headers().get("x-opt").is_none());
718        assert_eq!(with_none.status(), StatusCode::OK);
719    }
720
721    #[test]
722    fn extension_into_response_parts_inserts_into_response_extensions() {
723        use crate::routing::extract::Extension;
724
725        #[derive(Clone)]
726        struct Marker(u32);
727
728        let resp = (Extension(Marker(42)), "body").into_response();
729        assert_eq!(resp.extensions().get::<Marker>().unwrap().0, 42);
730    }
731
732    #[test]
733    fn response_parts_extensions_mut_is_reachable_directly() {
734        let mut parts = ResponseParts {
735            res: Response::new(Body::empty()),
736        };
737        let _ = parts.extensions_mut().insert(7u32);
738        assert_eq!(parts.res.extensions().get::<u32>(), Some(&7));
739    }
740
741    #[test]
742    fn append_headers_appends_without_replacing() {
743        let mut existing = HeaderMap::new();
744        let _ = existing.insert("x-multi", HeaderValue::from_static("first"));
745
746        let resp = (
747            existing,
748            AppendHeaders([("x-multi", "second"), ("x-other", "value")]),
749            "body",
750        )
751            .into_response();
752
753        let all: Vec<_> = resp
754            .headers()
755            .get_all("x-multi")
756            .iter()
757            .map(|v| v.to_str().unwrap())
758            .collect();
759        assert_eq!(all, vec!["first", "second"]);
760        assert_eq!(resp.headers().get("x-other").unwrap(), "value");
761    }
762
763    #[test]
764    fn append_headers_rejects_an_invalid_header_name() {
765        let resp = (AppendHeaders([("bad header name", "value")]), "body").into_response();
766        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
767    }
768
769    #[test]
770    fn append_headers_rejects_an_invalid_header_value() {
771        let resp = (AppendHeaders([("x-ok-name", "bad\nvalue")]), "body").into_response();
772        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
773    }
774
775    #[cfg(feature = "json")]
776    #[test]
777    fn json_response_shrinks_its_thread_local_buffer_after_a_large_payload() {
778        // Drives the JSON writer buffer past 64 KiB so the post-serialize
779        // `shrink_to(1024)` branch actually runs, not just the common case.
780        let big = "x".repeat(80 * 1024);
781        let resp = Json(big).into_response();
782        assert_eq!(resp.status(), StatusCode::OK);
783
784        // A second, small payload on the same thread proves the buffer is still
785        // usable after being shrunk.
786        let resp2 = Json("small").into_response();
787        assert_eq!(resp2.status(), StatusCode::OK);
788    }
789}