Skip to main content

churust_core/
response.rs

1//! The buffered [`Response`] type and the [`IntoResponse`] conversion trait.
2//!
3//! A [`Response`] is the in-memory result of handling a request: a status, a
4//! header map, and a body buffered as [`Bytes`]. Handlers rarely build one by
5//! hand — instead they return any value implementing [`IntoResponse`] (a
6//! `&str`, `String`, [`StatusCode`], a `(StatusCode, T)` tuple, an
7//! [`Error`], or a [`Result`](crate::error::Result)) and the framework
8//! converts it for you.
9
10use crate::body::Body;
11use crate::error::Error;
12use bytes::Bytes;
13use http::header::{HeaderName, CONTENT_TYPE};
14use http::{HeaderMap, HeaderValue, StatusCode};
15
16/// A fully-buffered HTTP response: status line, headers, and an in-memory body.
17///
18/// The three fields are public so middleware can post-process a response (for
19/// example inserting a header). Construct one with [`Response::new`] (status
20/// only), [`Response::text`] (a `text/plain` body), or [`Response::bytes`] (an
21/// arbitrary content type), then refine it with the chainable
22/// [`with_status`](Response::with_status) and
23/// [`with_header`](Response::with_header) builders. Most handlers return an
24/// [`IntoResponse`] value instead of building this directly.
25///
26/// ```
27/// use churust_core::Response;
28/// use http::StatusCode;
29///
30/// let res = Response::text("created").with_status(StatusCode::CREATED);
31/// assert_eq!(res.status, StatusCode::CREATED);
32/// assert_eq!(res.body.as_slice(), Some(&b"created"[..]));
33/// ```
34#[derive(Debug)]
35pub struct Response {
36    /// The HTTP status code of the response.
37    pub status: StatusCode,
38    /// The response headers.
39    pub headers: HeaderMap,
40    /// The response body — buffered bytes or a lazy stream.
41    pub body: Body,
42}
43
44impl Response {
45    /// Create an empty-bodied response with the given `status` and no headers.
46    ///
47    /// ```
48    /// use churust_core::Response;
49    /// use http::StatusCode;
50    ///
51    /// let res = Response::new(StatusCode::NO_CONTENT);
52    /// assert_eq!(res.status, StatusCode::NO_CONTENT);
53    /// assert!(res.body.is_empty());
54    /// ```
55    pub fn new(status: StatusCode) -> Self {
56        Self {
57            status,
58            headers: HeaderMap::new(),
59            body: Body::empty(),
60        }
61    }
62
63    /// Create a `200 OK` response with a `text/plain; charset=utf-8` body.
64    ///
65    /// ```
66    /// use churust_core::Response;
67    /// use http::StatusCode;
68    ///
69    /// let res = Response::text("hello");
70    /// assert_eq!(res.status, StatusCode::OK);
71    /// assert_eq!(
72    ///     res.headers.get(http::header::CONTENT_TYPE).unwrap(),
73    ///     "text/plain; charset=utf-8"
74    /// );
75    /// ```
76    pub fn text(body: impl Into<String>) -> Self {
77        let mut r = Self::new(StatusCode::OK);
78        r.headers.insert(
79            CONTENT_TYPE,
80            HeaderValue::from_static("text/plain; charset=utf-8"),
81        );
82        r.body = Body::from(body.into());
83        r
84    }
85
86    /// Create a `200 OK` response with a raw byte body and an explicit
87    /// `Content-Type`. Use this for non-text payloads (JSON, images, etc.); the
88    /// `content_type` must be a `'static` string.
89    ///
90    /// ```
91    /// use churust_core::Response;
92    ///
93    /// let res = Response::bytes("application/octet-stream", vec![1u8, 2, 3]);
94    /// assert_eq!(res.body.as_slice(), Some(&[1u8, 2, 3][..]));
95    /// assert_eq!(
96    ///     res.headers.get(http::header::CONTENT_TYPE).unwrap(),
97    ///     "application/octet-stream"
98    /// );
99    /// ```
100    pub fn bytes(content_type: &'static str, body: impl Into<Bytes>) -> Self {
101        let mut r = Self::new(StatusCode::OK);
102        r.headers
103            .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
104        r.body = Body::from(body.into());
105        r
106    }
107
108    /// Create a `200 OK` response whose body is produced lazily from `stream`,
109    /// with an explicit `Content-Type`. Use for large or dynamic payloads.
110    ///
111    /// ```
112    /// use churust_core::{Body, Response};
113    /// use bytes::Bytes;
114    ///
115    /// let chunks = futures_util::stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from("hi"))]);
116    /// let res = Response::stream("text/plain", Body::from_stream(chunks));
117    /// assert!(res.body.as_bytes().is_none());
118    /// ```
119    pub fn stream(content_type: &'static str, body: Body) -> Self {
120        let mut r = Self::new(StatusCode::OK);
121        r.headers
122            .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
123        r.body = body;
124        r
125    }
126
127    /// Override the status code, returning `self` for chaining.
128    ///
129    /// ```
130    /// use churust_core::Response;
131    /// use http::StatusCode;
132    ///
133    /// let res = Response::text("teapot").with_status(StatusCode::IM_A_TEAPOT);
134    /// assert_eq!(res.status, StatusCode::IM_A_TEAPOT);
135    /// ```
136    pub fn with_status(mut self, status: StatusCode) -> Self {
137        self.status = status;
138        self
139    }
140
141    /// Append a `Set-Cookie` header.
142    ///
143    /// Appends rather than replaces, so several cookies each get their own
144    /// header — which is what the spec requires; folding them into one comma-
145    /// separated value does not work in practice.
146    pub fn with_cookie(mut self, cookie: crate::cookie::Cookie) -> Self {
147        if let Ok(v) = HeaderValue::from_str(&cookie.to_header_value()) {
148            self.headers.append(http::header::SET_COOKIE, v);
149        }
150        self
151    }
152
153    /// Insert (or replace) a header, returning `self` for chaining.
154    ///
155    /// ```
156    /// use churust_core::Response;
157    /// use http::{header::LOCATION, HeaderValue};
158    ///
159    /// let res = Response::new(http::StatusCode::FOUND)
160    ///     .with_header(LOCATION, HeaderValue::from_static("/login"));
161    /// assert_eq!(res.headers.get(LOCATION).unwrap(), "/login");
162    /// ```
163    pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
164        self.headers.insert(name, value);
165        self
166    }
167
168    /// Add a field name to `Vary` without disturbing what is already there.
169    ///
170    /// Middleware that varies its output on a request header has to say so, and
171    /// more than one layer can want to. Each has to merge rather than overwrite:
172    /// a plugin that inserted its own field alone erased the other's, and a
173    /// shared cache then served a response keyed on the wrong thing — compressed
174    /// bytes handed to a client that never said it could decode them, or one
175    /// origin's response handed to another.
176    ///
177    /// Merging is here, in one place, because the merge is only correct if every
178    /// layer does it the same way: field names are case-insensitive, so the
179    /// comparison is too, and the value is appended in lower case so a response
180    /// passing through several layers reads as one consistent list rather than a
181    /// mixture. Two implementations agreeing by convention is what this replaces.
182    ///
183    /// Already-present fields and a `Vary: *` — which varies on everything — are
184    /// left alone.
185    ///
186    /// ```
187    /// use churust_core::Response;
188    ///
189    /// let mut res = Response::text("ok");
190    /// res.vary_on("accept-encoding");
191    /// res.vary_on("Origin");
192    /// assert_eq!(res.headers.get("vary").unwrap(), "accept-encoding, origin");
193    ///
194    /// // Asking twice changes nothing, whatever the spelling.
195    /// res.vary_on("ORIGIN");
196    /// assert_eq!(res.headers.get("vary").unwrap(), "accept-encoding, origin");
197    /// ```
198    pub fn vary_on(&mut self, field: &str) {
199        let field = field.trim().to_ascii_lowercase();
200        if field.is_empty() {
201            return;
202        }
203
204        let existing: Vec<String> = self
205            .headers
206            .get_all(http::header::VARY)
207            .iter()
208            .filter_map(|v| v.to_str().ok())
209            .flat_map(|v| v.split(','))
210            .map(|v| v.trim().to_ascii_lowercase())
211            .filter(|v| !v.is_empty())
212            .collect();
213
214        if existing.iter().any(|v| v == "*" || *v == field) {
215            return;
216        }
217
218        let mut merged = existing;
219        merged.push(field);
220        if let Ok(value) = HeaderValue::from_str(&merged.join(", ")) {
221            self.headers.insert(http::header::VARY, value);
222        }
223    }
224}
225
226/// Convert a handler return value into a [`Response`].
227///
228/// This is what lets handlers return ergonomic values instead of building a
229/// [`Response`] by hand. The crate implements it for the common cases:
230///
231/// - `()` — an empty `200 OK`.
232/// - `&'static str` / [`String`] — a `text/plain` body.
233/// - [`StatusCode`] — an empty body with that status.
234/// - `(StatusCode, T)` where `T: IntoResponse` — `T`'s response with the status
235///   overridden.
236/// - [`Error`] — rendered using its own status, message, and
237///   headers.
238/// - [`Result<T>`](crate::Result) where `T: IntoResponse` — the `Ok` value's
239///   response, or the `Err`'s.
240/// - [`Response`] — returned unchanged (identity).
241///
242/// Implement it for your own types to make them returnable from handlers.
243///
244/// ```
245/// use churust_core::{IntoResponse, Response};
246/// use http::StatusCode;
247///
248/// // The `(StatusCode, &str)` tuple impl in action:
249/// let res = (StatusCode::CREATED, "made").into_response();
250/// assert_eq!(res.status, StatusCode::CREATED);
251/// assert_eq!(res.body.as_slice(), Some(&b"made"[..]));
252/// ```
253pub trait IntoResponse {
254    /// Consume `self` and produce the [`Response`] to send.
255    fn into_response(self) -> Response;
256}
257
258impl IntoResponse for Response {
259    fn into_response(self) -> Response {
260        self
261    }
262}
263
264impl IntoResponse for () {
265    fn into_response(self) -> Response {
266        Response::new(StatusCode::OK)
267    }
268}
269
270impl IntoResponse for &'static str {
271    fn into_response(self) -> Response {
272        Response::text(self)
273    }
274}
275
276impl IntoResponse for String {
277    fn into_response(self) -> Response {
278        Response::text(self)
279    }
280}
281
282impl IntoResponse for StatusCode {
283    fn into_response(self) -> Response {
284        Response::new(self)
285    }
286}
287
288impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
289    fn into_response(self) -> Response {
290        let (status, inner) = self;
291        inner.into_response().with_status(status)
292    }
293}
294
295impl IntoResponse for Error {
296    fn into_response(self) -> Response {
297        let mut res = Response::text(self.message().to_string()).with_status(self.status());
298        // First occurrence of a name replaces, the rest append.
299        //
300        // `Error::with_response_header` is documented as callable repeatedly, and
301        // its store is a `Vec`, so an error can legitimately carry two values of
302        // one name — two `Set-Cookie`s, or two `WWW-Authenticate` challenges when
303        // a route accepts more than one scheme. `insert` for every pair kept only
304        // the last of those, which is the same defect `Response::with_cookie`
305        // already avoids: folding several cookies into one comma-separated value
306        // "does not work in practice", and silently dropping all but one is worse.
307        //
308        // Not a blanket `append`, though, which would be the obvious fix and is
309        // wrong. `Response::text` above has already set `Content-Type`, so an
310        // error carrying `with_response_header(CONTENT_TYPE, "application/json")`
311        // would go out with two of them and the recipient would have to guess.
312        // Replacing on the first sighting keeps that an override; appending
313        // afterwards keeps a deliberate repeat.
314        let mut replaced: Vec<&HeaderName> = Vec::new();
315        for (name, value) in self.response_headers() {
316            if replaced.contains(&name) {
317                res.headers.append(name.clone(), value.clone());
318            } else {
319                res.headers.insert(name.clone(), value.clone());
320                replaced.push(name);
321            }
322        }
323        res
324    }
325}
326
327/// A `Result` whose `Ok`/`Err` both render to a response.
328impl<T: IntoResponse> IntoResponse for crate::error::Result<T> {
329    fn into_response(self) -> Response {
330        match self {
331            Ok(v) => v.into_response(),
332            Err(e) => e.into_response(),
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn text_sets_content_type_and_body() {
343        let r = Response::text("hi");
344        assert_eq!(r.status, StatusCode::OK);
345        assert_eq!(r.body, Bytes::from("hi"));
346        assert_eq!(
347            r.headers.get(CONTENT_TYPE).unwrap(),
348            "text/plain; charset=utf-8"
349        );
350    }
351
352    #[test]
353    fn status_tuple_overrides_status() {
354        let r = (StatusCode::CREATED, "made").into_response();
355        assert_eq!(r.status, StatusCode::CREATED);
356        assert_eq!(r.body, Bytes::from("made"));
357    }
358
359    #[test]
360    fn error_renders_with_its_status() {
361        let r = Error::bad_request("x").into_response();
362        assert_eq!(r.status, StatusCode::BAD_REQUEST);
363    }
364
365    #[test]
366    fn an_error_carrying_two_of_one_header_renders_both() {
367        let r = Error::new(StatusCode::UNAUTHORIZED, "no")
368            .with_response_header(
369                http::header::WWW_AUTHENTICATE,
370                HeaderValue::from_static("Basic realm=\"api\""),
371            )
372            .with_response_header(
373                http::header::WWW_AUTHENTICATE,
374                HeaderValue::from_static("Bearer"),
375            )
376            .into_response();
377
378        let challenges: Vec<_> = r
379            .headers
380            .get_all(http::header::WWW_AUTHENTICATE)
381            .iter()
382            .map(|v| v.to_str().unwrap())
383            .collect();
384        assert_eq!(
385            challenges,
386            vec!["Basic realm=\"api\"", "Bearer"],
387            "both challenges should reach the client, in order"
388        );
389    }
390
391    #[test]
392    fn an_error_header_still_overrides_the_body_content_type() {
393        // The discriminating one. A blanket `append` passes the test above and
394        // fails this: `Response::text` has already set `text/plain`, so the
395        // override has to replace it rather than sit beside it.
396        let r = Error::bad_request("nope")
397            .with_response_header(
398                http::header::CONTENT_TYPE,
399                HeaderValue::from_static("application/json"),
400            )
401            .into_response();
402
403        assert_eq!(
404            r.headers.get_all(http::header::CONTENT_TYPE).iter().count(),
405            1,
406            "an overriding header must not be appended beside the one it overrides"
407        );
408        assert_eq!(
409            r.headers.get(http::header::CONTENT_TYPE).unwrap(),
410            "application/json"
411        );
412    }
413}