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<Vec<u8>> = std::cell::RefCell::new(Vec::with_capacity(1024));
227}
228
229/// A JSON response. Requires the `json` feature.
230#[cfg(feature = "json")]
231pub use crate::routing::extract::Json;
232
233#[cfg(feature = "json")]
234impl<T> IntoResponse for Json<T>
235where
236    T: Serialize,
237{
238    fn into_response(self) -> Response<Body> {
239        // `try_borrow_mut` (rather than `borrow_mut`) so that a `Serialize` impl
240        // which re-enters this function on the same thread (e.g. by serializing a
241        // nested `Json<_>` internally) falls back to a fresh, non-shared buffer
242        // instead of panicking on an already-borrowed `RefCell` — a panic path the
243        // crate's `deny(clippy::panic/unwrap_used/expect_used)` lints can't catch
244        // since it originates inside `RefCell` itself, not an explicit unwrap.
245        #[allow(clippy::single_match_else)]
246        let result = JSON_WRITE_BUF.with(|buf| match buf.try_borrow_mut() {
247            Ok(mut b) => {
248                b.clear();
249                let res = match serde_json::to_writer(&mut *b, &self.0) {
250                    Ok(()) => Ok(Bytes::copy_from_slice(&b)),
251                    Err(err) => Err(err),
252                };
253                if b.capacity() > 65536 {
254                    b.shrink_to(1024);
255                }
256                res
257            }
258            Err(_) => {
259                let mut b = Vec::with_capacity(1024);
260                match serde_json::to_writer(&mut b, &self.0) {
261                    Ok(()) => Ok(Bytes::from(b)),
262                    Err(err) => Err(err),
263                }
264            }
265        });
266
267        match result {
268            Ok(bytes) => {
269                let mut res = Response::new(Body::full(bytes));
270                let _ = res
271                    .headers_mut()
272                    .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
273                res
274            }
275            Err(err) => {
276                let mut res = Response::new(Body::full(Bytes::from(format!(
277                    "Failed to serialize JSON: {err}"
278                ))));
279                *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
280                let _ = res.headers_mut().insert(
281                    CONTENT_TYPE,
282                    HeaderValue::from_static("text/plain; charset=utf-8"),
283                );
284                res
285            }
286        }
287    }
288}
289
290impl<R> IntoResponse for (StatusCode, R)
291where
292    R: IntoResponse,
293{
294    fn into_response(self) -> Response<Body> {
295        let (status, res) = self;
296        let mut response = res.into_response();
297        *response.status_mut() = status;
298        response
299    }
300}
301
302// ─── IntoResponseParts ─────────────────────────────────────────────────────────
303//
304// Generalizes response-tuple composition beyond a fixed whitelist of shapes.
305// Any type implementing `IntoResponseParts` (headers, cookies, extensions, or a
306// user's own typed-header wrapper) can be combined — in any order, up to 8 of
307// them — with an optional leading `StatusCode` and a trailing body, mirroring
308// Axum's `IntoResponseParts` design. e.g. `(HeaderMap, Cookies, Json<T>)` and
309// `(StatusCode, Cookies, HeaderMap, Json<T>)` both just work.
310
311/// The response under construction, passed to [`IntoResponseParts::into_response_parts`]
312/// so implementors can attach headers/extensions without unpacking the whole response.
313#[derive(Debug)]
314pub struct ResponseParts {
315    res: Response<Body>,
316}
317
318impl ResponseParts {
319    /// Mutable access to the headers of the response being built.
320    pub fn headers_mut(&mut self) -> &mut HeaderMap {
321        self.res.headers_mut()
322    }
323
324    /// Mutable access to the extensions of the response being built.
325    pub fn extensions_mut(&mut self) -> &mut hyper::http::Extensions {
326        self.res.extensions_mut()
327    }
328}
329
330/// Trait for types that attach headers/extensions to a response without providing
331/// its body — the building block for flexible, order-independent response tuples.
332///
333/// Implement this for your own typed-header wrappers to use them anywhere in a
334/// response tuple, e.g. `(MyCacheControl, StatusCode, Json<T>)`.
335pub trait IntoResponseParts {
336    /// The rejection response returned if attaching the parts fails.
337    type Error: IntoResponse;
338
339    /// Attach `self` onto `res`, returning the updated parts (or a rejection).
340    ///
341    /// # Errors
342    /// Returns `Self::Error` if the parts cannot be attached (e.g. an invalid header value).
343    fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error>;
344}
345
346impl IntoResponseParts for HeaderMap {
347    type Error = std::convert::Infallible;
348
349    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
350        res.headers_mut().extend(self);
351        Ok(res)
352    }
353}
354
355impl<T> IntoResponseParts for Option<T>
356where
357    T: IntoResponseParts,
358{
359    type Error = T::Error;
360
361    fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
362        match self {
363            Some(parts) => parts.into_response_parts(res),
364            None => Ok(res),
365        }
366    }
367}
368
369impl<T> IntoResponseParts for crate::routing::extract::Extension<T>
370where
371    T: Clone + Send + Sync + 'static,
372{
373    type Error = std::convert::Infallible;
374
375    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
376        res.extensions_mut().insert(self.0);
377        Ok(res)
378    }
379}
380
381/// Appends an arbitrary collection of headers to a response without
382/// replacing any existing header of the same name, matching
383/// `axum::response::AppendHeaders`.
384///
385/// A bare `HeaderMap` already appends (see the `IntoResponseParts` impl
386/// above) — this exists for the common case of a small, fixed list of
387/// `(name, value)` pairs (e.g. `[("x-custom", "1"), ("x-other", "2")]`)
388/// without constructing a full `HeaderMap` first.
389#[derive(Debug, Clone, Copy)]
390pub struct AppendHeaders<I>(pub I);
391
392impl<I, K, V> IntoResponseParts for AppendHeaders<I>
393where
394    I: IntoIterator<Item = (K, V)>,
395    K: TryInto<HeaderName>,
396    V: TryInto<HeaderValue>,
397{
398    type Error = crate::http::error::Error;
399
400    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
401        for (key, value) in self.0 {
402            let key = key
403                .try_into()
404                .map_err(|_| crate::http::error::Error::Rejection {
405                    status: StatusCode::INTERNAL_SERVER_ERROR,
406                    message: "AppendHeaders: invalid header name".to_string(),
407                })?;
408            let value = value
409                .try_into()
410                .map_err(|_| crate::http::error::Error::Rejection {
411                    status: StatusCode::INTERNAL_SERVER_ERROR,
412                    message: "AppendHeaders: invalid header value".to_string(),
413                })?;
414            res.headers_mut().append(key, value);
415        }
416        Ok(res)
417    }
418}
419
420#[cfg(feature = "cookies")]
421impl IntoResponseParts for crate::routing::extract::Cookies {
422    type Error = std::convert::Infallible;
423
424    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
425        for cookie in self.jar.delta() {
426            if let Ok(header_val) = HeaderValue::try_from(cookie.encoded().to_string()) {
427                res.headers_mut()
428                    .append(hyper::header::SET_COOKIE, header_val);
429            }
430        }
431        Ok(res)
432    }
433}
434
435macro_rules! impl_into_response_for_parts_tuples {
436    ($($T:ident),+) => {
437        impl<R, $($T),+> IntoResponse for ($($T,)+ R)
438        where
439            R: IntoResponse,
440            $( $T: IntoResponseParts, )+
441        {
442            fn into_response(self) -> Response<Body> {
443                #[allow(non_snake_case)]
444                let ($($T,)+ res) = self;
445                let mut parts = ResponseParts { res: res.into_response() };
446                $(
447                    parts = match $T.into_response_parts(parts) {
448                        Ok(p) => p,
449                        Err(rejection) => return rejection.into_response(),
450                    };
451                )+
452                parts.res
453            }
454        }
455
456        impl<R, $($T),+> IntoResponse for (StatusCode, $($T,)+ R)
457        where
458            R: IntoResponse,
459            $( $T: IntoResponseParts, )+
460        {
461            fn into_response(self) -> Response<Body> {
462                #[allow(non_snake_case)]
463                let (status, $($T,)+ res) = self;
464                let mut response = <($($T,)+ R) as IntoResponse>::into_response(($($T,)+ res));
465                *response.status_mut() = status;
466                response
467            }
468        }
469    };
470}
471
472impl_into_response_for_parts_tuples!(T1);
473impl_into_response_for_parts_tuples!(T1, T2);
474impl_into_response_for_parts_tuples!(T1, T2, T3);
475impl_into_response_for_parts_tuples!(T1, T2, T3, T4);
476impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5);
477impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5, T6);
478impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5, T6, T7);
479impl_into_response_for_parts_tuples!(T1, T2, T3, T4, T5, T6, T7, T8);
480
481impl IntoResponse for () {
482    fn into_response(self) -> Response<Body> {
483        Response::builder()
484            .status(StatusCode::OK)
485            .body(Body::empty())
486            .unwrap_or_else(|_| Response::new(Body::empty()))
487    }
488}
489
490impl<T, E> IntoResponse for Result<T, E>
491where
492    T: IntoResponse,
493    E: IntoResponse,
494{
495    fn into_response(self) -> Response<Body> {
496        match self {
497            Ok(value) => value.into_response(),
498            Err(err) => err.into_response(),
499        }
500    }
501}
502
503impl IntoResponse for std::convert::Infallible {
504    fn into_response(self) -> Response<Body> {
505        match self {}
506    }
507}
508
509/// Response that redirects the client to another location.
510#[derive(Debug, Clone)]
511pub struct Redirect {
512    status_code: StatusCode,
513    location: HeaderValue,
514}
515
516impl Redirect {
517    /// Create a `303 See Other` redirect to the given URI.
518    ///
519    /// If `uri` contains bytes that aren't valid in an HTTP header value (e.g. a
520    /// stray `\n` or `\r`), this crate's `deny(clippy::panic)` policy rules out
521    /// panicking the way Axum's equivalent does — instead the `Location` header
522    /// silently falls back to `/`. Validate/sanitize `uri` yourself if it's ever
523    /// built from user-controlled or templated data, since a silent fallback to
524    /// `/` is easy to miss.
525    #[must_use]
526    pub fn to(uri: &str) -> Self {
527        Self {
528            status_code: StatusCode::SEE_OTHER,
529            location: HeaderValue::try_from(uri).unwrap_or_else(|_| HeaderValue::from_static("/")),
530        }
531    }
532
533    /// Create a `307 Temporary Redirect` redirect to the given URI.
534    ///
535    /// See [`Redirect::to`]'s docs for the fallback behavior on an invalid `uri`.
536    #[must_use]
537    pub fn temporary(uri: &str) -> Self {
538        Self {
539            status_code: StatusCode::TEMPORARY_REDIRECT,
540            location: HeaderValue::try_from(uri).unwrap_or_else(|_| HeaderValue::from_static("/")),
541        }
542    }
543
544    /// Create a `308 Permanent Redirect` redirect to the given URI.
545    ///
546    /// See [`Redirect::to`]'s docs for the fallback behavior on an invalid `uri`.
547    #[must_use]
548    pub fn permanent(uri: &str) -> Self {
549        Self {
550            status_code: StatusCode::PERMANENT_REDIRECT,
551            location: HeaderValue::try_from(uri).unwrap_or_else(|_| HeaderValue::from_static("/")),
552        }
553    }
554}
555
556impl IntoResponse for Redirect {
557    fn into_response(self) -> Response<Body> {
558        let mut resp = Response::new(Body::empty());
559        *resp.status_mut() = self.status_code;
560        let _ = resp
561            .headers_mut()
562            .insert(hyper::header::LOCATION, self.location);
563        resp
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use crate::routing::extract::Cookies;
571    use hyper::HeaderMap;
572
573    #[test]
574    fn test_body_debug_and_size_hint() {
575        let b1 = Body::empty();
576        assert!(format!("{b1:?}").contains("Empty"));
577        assert!(b1.is_end_stream());
578        assert_eq!(b1.size_hint().exact(), Some(0));
579
580        let b2 = Body::full(Bytes::from("test"));
581        assert!(format!("{b2:?}").contains("Full"));
582        assert!(!b2.is_end_stream());
583        assert_eq!(b2.size_hint().exact(), Some(4));
584
585        let stream_body = BoxBody::new(
586            http_body_util::Empty::<Bytes>::new()
587                .map_err(|e| crate::http::error::Error::Internal(e.to_string())),
588        );
589        let b3 = Body::Stream(stream_body);
590        assert!(format!("{b3:?}").contains("Stream"));
591        assert!(b3.is_end_stream());
592        assert_eq!(b3.size_hint().exact(), Some(0));
593    }
594
595    #[tokio::test]
596    async fn test_body_poll_frame() {
597        use hyper::body::Body as _;
598        let mut b1 = Body::full(Bytes::from("a"));
599        let mut b1_pin = Pin::new(&mut b1);
600        let cx = &mut Context::from_waker(futures::task::noop_waker_ref());
601        let f1 = b1_pin.as_mut().poll_frame(cx);
602        assert!(matches!(f1, Poll::Ready(Some(Ok(_)))));
603        let f2 = b1_pin.as_mut().poll_frame(cx);
604        assert!(matches!(f2, Poll::Ready(None)));
605
606        let mut b2 = Body::empty();
607        let f3 = Pin::new(&mut b2).poll_frame(cx);
608        assert!(matches!(f3, Poll::Ready(None)));
609    }
610
611    #[cfg(feature = "json")]
612    struct FailSerialize;
613    #[cfg(feature = "json")]
614    impl Serialize for FailSerialize {
615        fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
616        where
617            S: serde::Serializer,
618        {
619            Err(serde::ser::Error::custom("failed"))
620        }
621    }
622
623    #[test]
624    fn test_into_response_implementations() {
625        let full_resp = Response::new(Full::new(Bytes::from("abc")));
626        let r1 = full_resp.into_response();
627        assert_eq!(r1.status(), StatusCode::OK);
628
629        let v: Vec<u8> = vec![1, 2, 3];
630        let r2 = v.into_response();
631        assert_eq!(
632            r2.headers().get(CONTENT_TYPE).unwrap(),
633            "application/octet-stream"
634        );
635
636        let s: &'static [u8] = b"static";
637        let r3 = s.into_response();
638        assert_eq!(
639            r3.headers().get(CONTENT_TYPE).unwrap(),
640            "application/octet-stream"
641        );
642
643        #[cfg(feature = "json")]
644        {
645            let fail_json = Json(FailSerialize);
646            let r4 = fail_json.into_response();
647            assert_eq!(r4.status(), StatusCode::INTERNAL_SERVER_ERROR);
648        }
649
650        let mut h = HeaderMap::new();
651        let _ = h.insert("x-custom", HeaderValue::from_static("val"));
652        let r5 = (h.clone(), "body").into_response();
653        assert_eq!(r5.headers().get("x-custom").unwrap(), "val");
654
655        let r6 = (StatusCode::CREATED, h.clone(), "body").into_response();
656        assert_eq!(r6.status(), StatusCode::CREATED);
657        assert_eq!(r6.headers().get("x-custom").unwrap(), "val");
658
659        let cookies = Cookies::new();
660        let r7 = (StatusCode::ACCEPTED, cookies, "body").into_response();
661        assert_eq!(r7.status(), StatusCode::ACCEPTED);
662
663        let r8 = ().into_response();
664        assert_eq!(r8.status(), StatusCode::OK);
665
666        let res_ok: Result<&str, &str> = Ok("ok");
667        let r9 = res_ok.into_response();
668        assert_eq!(r9.status(), StatusCode::OK);
669
670        let res_err: Result<&str, &str> = Err("err");
671        let r10 = res_err.into_response();
672        assert_eq!(r10.status(), StatusCode::OK);
673    }
674
675    #[test]
676    fn option_into_response_parts_some_and_none() {
677        let mut h = HeaderMap::new();
678        let _ = h.insert("x-opt", HeaderValue::from_static("present"));
679
680        let with_some = (Some(h), "body").into_response();
681        assert_eq!(with_some.headers().get("x-opt").unwrap(), "present");
682
683        let with_none = (None::<HeaderMap>, "body").into_response();
684        assert!(with_none.headers().get("x-opt").is_none());
685        assert_eq!(with_none.status(), StatusCode::OK);
686    }
687
688    #[test]
689    fn extension_into_response_parts_inserts_into_response_extensions() {
690        use crate::routing::extract::Extension;
691
692        #[derive(Clone)]
693        struct Marker(u32);
694
695        let resp = (Extension(Marker(42)), "body").into_response();
696        assert_eq!(resp.extensions().get::<Marker>().unwrap().0, 42);
697    }
698
699    #[test]
700    fn response_parts_extensions_mut_is_reachable_directly() {
701        let mut parts = ResponseParts {
702            res: Response::new(Body::empty()),
703        };
704        let _ = parts.extensions_mut().insert(7u32);
705        assert_eq!(parts.res.extensions().get::<u32>(), Some(&7));
706    }
707
708    #[test]
709    fn append_headers_appends_without_replacing() {
710        let mut existing = HeaderMap::new();
711        let _ = existing.insert("x-multi", HeaderValue::from_static("first"));
712
713        let resp = (
714            existing,
715            AppendHeaders([("x-multi", "second"), ("x-other", "value")]),
716            "body",
717        )
718            .into_response();
719
720        let all: Vec<_> = resp
721            .headers()
722            .get_all("x-multi")
723            .iter()
724            .map(|v| v.to_str().unwrap())
725            .collect();
726        assert_eq!(all, vec!["first", "second"]);
727        assert_eq!(resp.headers().get("x-other").unwrap(), "value");
728    }
729
730    #[test]
731    fn append_headers_rejects_an_invalid_header_name() {
732        let resp = (AppendHeaders([("bad header name", "value")]), "body").into_response();
733        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
734    }
735
736    #[test]
737    fn append_headers_rejects_an_invalid_header_value() {
738        let resp = (AppendHeaders([("x-ok-name", "bad\nvalue")]), "body").into_response();
739        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
740    }
741
742    #[cfg(feature = "json")]
743    #[test]
744    fn json_response_shrinks_its_thread_local_buffer_after_a_large_payload() {
745        // Drives the JSON writer buffer past 64 KiB so the post-serialize
746        // `shrink_to(1024)` branch actually runs, not just the common case.
747        let big = "x".repeat(80 * 1024);
748        let resp = Json(big).into_response();
749        assert_eq!(resp.status(), StatusCode::OK);
750
751        // A second, small payload on the same thread proves the buffer is still
752        // usable after being shrunk.
753        let resp2 = Json("small").into_response();
754        assert_eq!(resp2.status(), StatusCode::OK);
755    }
756}