Skip to main content

rainier_http/
response.rs

1//! The outgoing [`Response`], and [`IntoResponse`] for returning one from a
2//! handler.
3//!
4//! A controller action rarely wants to build a `Response` by hand, so anything
5//! reasonable converts into one: a string, a `serde_json::Value`, a
6//! [`Json<T>`], a [`Redirect`], a `StatusCode`, a `Result`, or a tuple pairing
7//! a status with any of those.
8
9use bytes::Bytes;
10use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
11use rainier_support::{Error, Extensions};
12use serde::Serialize;
13
14use crate::body::Body;
15use crate::cookie::Cookie;
16
17/// An outgoing HTTP response.
18#[derive(Debug)]
19pub struct Response {
20    status: StatusCode,
21    headers: HeaderMap,
22    body: Body,
23    extensions: Extensions,
24}
25
26impl Response {
27    /// An empty response with `status`.
28    pub fn new(status: StatusCode) -> Self {
29        Self { status, headers: HeaderMap::new(), body: Body::Empty, extensions: Extensions::new() }
30    }
31
32    /// `200 OK` with `body`.
33    pub fn ok(body: impl Into<Body>) -> Self {
34        Self::new(StatusCode::OK).with_body(body)
35    }
36
37    /// `204 No Content`.
38    pub fn no_content() -> Self {
39        Self::new(StatusCode::NO_CONTENT)
40    }
41
42    /// `200 OK` with a `text/plain` body.
43    pub fn text(body: impl Into<String>) -> Self {
44        Self::ok(body.into()).with_content_type("text/plain; charset=utf-8")
45    }
46
47    /// `200 OK` with a `text/html` body.
48    pub fn html(body: impl Into<String>) -> Self {
49        Self::ok(body.into()).with_content_type("text/html; charset=utf-8")
50    }
51
52    /// `200 OK` with `value` serialised as JSON.
53    ///
54    /// A value that cannot be serialised becomes a `500` describing the
55    /// failure, because there is nowhere else for the error to go — the
56    /// signature has to produce a response.
57    pub fn json(value: &impl Serialize) -> Self {
58        match serde_json::to_vec(value) {
59            Ok(encoded) => Self::ok(encoded).with_content_type("application/json; charset=utf-8"),
60            Err(e) => Error::internal(format!("could not serialise the response: {e}")).into(),
61        }
62    }
63
64    /// `201 Created`, with a `Location` header.
65    pub fn created(location: impl AsRef<str>) -> Self {
66        Self::new(StatusCode::CREATED).with_header("location", location.as_ref())
67    }
68
69    /// A response that offers the body as a file download.
70    pub fn download(bytes: impl Into<Bytes>, file_name: &str) -> Self {
71        // Quote the name and strip quotes/newlines from it: an unescaped `"`
72        // or CRLF here would let a caller inject header content.
73        let safe: String =
74            file_name.chars().filter(|c| *c != '"' && *c != '\r' && *c != '\n').collect();
75        Self::ok(bytes.into())
76            .with_content_type("application/octet-stream")
77            .with_header("content-disposition", &format!("attachment; filename=\"{safe}\""))
78    }
79
80    /// A streaming response.
81    pub fn stream<S>(stream: S) -> Self
82    where
83        S: futures_core::Stream<Item = Result<Bytes, Error>> + Send + 'static,
84    {
85        Self::new(StatusCode::OK).with_body(Body::from_stream(stream))
86    }
87
88    // --- accessors ---------------------------------------------------------
89
90    /// The status code.
91    pub fn status(&self) -> StatusCode {
92        self.status
93    }
94
95    /// The headers.
96    pub fn headers(&self) -> &HeaderMap {
97        &self.headers
98    }
99
100    /// Mutable headers.
101    pub fn headers_mut(&mut self) -> &mut HeaderMap {
102        &mut self.headers
103    }
104
105    /// One header as a string.
106    pub fn header(&self, name: &str) -> Option<&str> {
107        self.headers.get(name)?.to_str().ok()
108    }
109
110    /// The body.
111    pub fn body(&self) -> &Body {
112        &self.body
113    }
114
115    /// Take the body, leaving an empty one behind.
116    pub fn take_body(&mut self) -> Body {
117        std::mem::take(&mut self.body)
118    }
119
120    /// Response attributes, keyed by type.
121    pub fn extensions(&self) -> &Extensions {
122        &self.extensions
123    }
124
125    /// Mutable response attributes.
126    pub fn extensions_mut(&mut self) -> &mut Extensions {
127        &mut self.extensions
128    }
129
130    /// Whether the status is 2xx.
131    pub fn is_successful(&self) -> bool {
132        self.status.is_success()
133    }
134
135    // --- builders ----------------------------------------------------------
136
137    /// Replace the status.
138    pub fn with_status(mut self, status: StatusCode) -> Self {
139        self.status = status;
140        self
141    }
142
143    /// Replace the body.
144    pub fn with_body(mut self, body: impl Into<Body>) -> Self {
145        self.body = body.into();
146        self
147    }
148
149    /// Set a header, replacing any existing one of that name.
150    ///
151    /// An invalid name or value is dropped rather than panicking: header
152    /// content often comes from configuration or user data, and taking down a
153    /// request for it would be worse than omitting it.
154    pub fn with_header(mut self, name: &str, value: &str) -> Self {
155        if let (Ok(name), Ok(value)) = (HeaderName::try_from(name), HeaderValue::from_str(value)) {
156            self.headers.insert(name, value);
157        }
158        self
159    }
160
161    /// Append a header, keeping any existing ones of that name.
162    pub fn with_added_header(mut self, name: &str, value: &str) -> Self {
163        if let (Ok(name), Ok(value)) = (HeaderName::try_from(name), HeaderValue::from_str(value)) {
164            self.headers.append(name, value);
165        }
166        self
167    }
168
169    /// Set `Content-Type`.
170    pub fn with_content_type(self, content_type: &str) -> Self {
171        self.with_header("content-type", content_type)
172    }
173
174    /// Attach a `Set-Cookie` header. Several cookies may be attached.
175    pub fn with_cookie(self, cookie: &Cookie) -> Self {
176        self.with_added_header("set-cookie", &cookie.to_set_cookie())
177    }
178
179    /// Instruct the client to drop a cookie.
180    pub fn without_cookie(self, name: &str) -> Self {
181        self.with_cookie(&Cookie::removal(name))
182    }
183
184    /// Convert into an `http::Response`, ready for the server to write.
185    pub fn into_http(self) -> http::Response<Body> {
186        let mut builder = http::Response::builder().status(self.status);
187        if let Some(headers) = builder.headers_mut() {
188            *headers = self.headers;
189        }
190        builder.body(self.body).unwrap_or_else(|_| {
191            // Unreachable: the status came from a `StatusCode` and the headers
192            // from a `HeaderMap`, both already validated.
193            http::Response::new(Body::Empty)
194        })
195    }
196}
197
198impl Default for Response {
199    fn default() -> Self {
200        Self::new(StatusCode::OK)
201    }
202}
203
204/// A redirect response.
205#[derive(Debug, Clone)]
206pub struct Redirect {
207    status: StatusCode,
208    location: String,
209}
210
211impl Redirect {
212    /// `302 Found` — a temporary redirect that clients may re-issue as GET.
213    pub fn to(location: impl Into<String>) -> Self {
214        Self { status: StatusCode::FOUND, location: location.into() }
215    }
216
217    /// `301 Moved Permanently`.
218    pub fn permanent(location: impl Into<String>) -> Self {
219        Self { status: StatusCode::MOVED_PERMANENTLY, location: location.into() }
220    }
221
222    /// `303 See Other` — the correct redirect after a successful POST, since
223    /// it tells the client to follow up with a GET.
224    pub fn see_other(location: impl Into<String>) -> Self {
225        Self { status: StatusCode::SEE_OTHER, location: location.into() }
226    }
227
228    /// `307 Temporary Redirect` — like 302 but the method is preserved.
229    pub fn temporary(location: impl Into<String>) -> Self {
230        Self { status: StatusCode::TEMPORARY_REDIRECT, location: location.into() }
231    }
232}
233
234/// A `text/html` body.
235#[derive(Debug, Clone)]
236pub struct Html<T>(pub T);
237
238/// An `application/json` body.
239#[derive(Debug, Clone)]
240pub struct Json<T>(pub T);
241
242/// Anything a handler can return.
243pub trait IntoResponse {
244    /// Build the response.
245    fn into_response(self) -> Response;
246}
247
248impl IntoResponse for Response {
249    fn into_response(self) -> Response {
250        self
251    }
252}
253
254impl IntoResponse for () {
255    fn into_response(self) -> Response {
256        Response::no_content()
257    }
258}
259
260impl IntoResponse for StatusCode {
261    fn into_response(self) -> Response {
262        Response::new(self)
263    }
264}
265
266impl IntoResponse for String {
267    fn into_response(self) -> Response {
268        Response::text(self)
269    }
270}
271
272impl IntoResponse for &'static str {
273    fn into_response(self) -> Response {
274        Response::text(self)
275    }
276}
277
278impl IntoResponse for serde_json::Value {
279    fn into_response(self) -> Response {
280        Response::json(&self)
281    }
282}
283
284impl<T: Serialize> IntoResponse for Json<T> {
285    fn into_response(self) -> Response {
286        Response::json(&self.0)
287    }
288}
289
290impl<T: Into<String>> IntoResponse for Html<T> {
291    fn into_response(self) -> Response {
292        Response::html(self.0)
293    }
294}
295
296impl IntoResponse for Redirect {
297    fn into_response(self) -> Response {
298        Response::new(self.status).with_header("location", &self.location)
299    }
300}
301
302impl IntoResponse for Body {
303    fn into_response(self) -> Response {
304        Response::ok(self)
305    }
306}
307
308impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
309    fn into_response(self) -> Response {
310        let (status, inner) = self;
311        inner.into_response().with_status(status)
312    }
313}
314
315impl<T: IntoResponse> IntoResponse for Option<T> {
316    /// `None` becomes a `404` — the usual meaning of "the handler found
317    /// nothing".
318    fn into_response(self) -> Response {
319        match self {
320            Some(inner) => inner.into_response(),
321            None => Error::not_found("Not Found").into_response(),
322        }
323    }
324}
325
326impl<T, E> IntoResponse for Result<T, E>
327where
328    T: IntoResponse,
329    E: IntoResponse,
330{
331    fn into_response(self) -> Response {
332        match self {
333            Ok(inner) => inner.into_response(),
334            Err(e) => e.into_response(),
335        }
336    }
337}
338
339/// Marks a response as having been rendered from a framework [`Error`], and
340/// carries the parts needed to render it *differently*.
341///
342/// Without this, the HTTP kernel could not tell a JSON error body it produced
343/// from one a handler deliberately returned, and so could not offer a browser
344/// an HTML error page instead. Attached to the response's extensions, so it
345/// never reaches the client.
346#[derive(Debug, Clone)]
347pub struct RenderedError {
348    /// The status the error rendered as.
349    pub status: u16,
350    /// The human-readable message.
351    pub message: String,
352    /// The structured details, if any.
353    pub details: Option<serde_json::Value>,
354    /// Whether the message is safe to show a client.
355    ///
356    /// `false` for 5xx: an internal error's message routinely contains a
357    /// connection string, a file path or a query. The kernel substitutes a
358    /// generic message unless the application is in debug mode.
359    pub disclosable: bool,
360}
361
362/// Renders a framework error as JSON.
363///
364/// The default, and what an API client should get. The HTTP kernel wraps it
365/// with content negotiation — an HTML error page for a browser — using the
366/// [`RenderedError`] this attaches.
367impl IntoResponse for Error {
368    fn into_response(self) -> Response {
369        let status =
370            StatusCode::from_u16(self.status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
371        let disclosable = !self.kind().is_server_error();
372
373        let mut payload = serde_json::Map::new();
374        payload.insert("message".into(), serde_json::Value::String(self.message().to_string()));
375        if let Some(details) = self.details() {
376            payload.insert("errors".into(), details.clone());
377        }
378
379        let mut response = Response::json(&serde_json::Value::Object(payload)).with_status(status);
380
381        response.extensions_mut().insert(RenderedError {
382            status: self.status(),
383            message: self.message().to_string(),
384            details: self.details().cloned(),
385            disclosable,
386        });
387        response
388    }
389}
390
391impl From<Error> for Response {
392    fn from(error: Error) -> Self {
393        error.into_response()
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use serde_json::json;
401
402    async fn body_of(response: Response) -> String {
403        String::from_utf8(response.body.collect().await.unwrap().to_vec()).unwrap()
404    }
405
406    #[tokio::test]
407    async fn builds_the_common_shapes() {
408        let text = Response::text("hi");
409        assert_eq!(text.status(), StatusCode::OK);
410        assert_eq!(text.header("content-type"), Some("text/plain; charset=utf-8"));
411        assert_eq!(body_of(text).await, "hi");
412
413        let html = Response::html("<p>hi</p>");
414        assert_eq!(html.header("content-type"), Some("text/html; charset=utf-8"));
415
416        let json = Response::json(&json!({ "a": 1 }));
417        assert_eq!(json.header("content-type"), Some("application/json; charset=utf-8"));
418        assert_eq!(body_of(json).await, r#"{"a":1}"#);
419
420        assert_eq!(Response::no_content().status(), StatusCode::NO_CONTENT);
421        assert_eq!(Response::created("/posts/1").header("location"), Some("/posts/1"));
422    }
423
424    #[test]
425    fn headers_replace_or_append() {
426        let response = Response::ok(()).with_header("x-a", "1").with_header("x-a", "2");
427        assert_eq!(response.headers().get_all("x-a").iter().count(), 1);
428        assert_eq!(response.header("x-a"), Some("2"));
429
430        let response = Response::ok(()).with_added_header("x-b", "1").with_added_header("x-b", "2");
431        assert_eq!(response.headers().get_all("x-b").iter().count(), 2);
432    }
433
434    #[test]
435    fn an_invalid_header_is_dropped_rather_than_panicking() {
436        let response = Response::ok(()).with_header("bad header name", "v");
437        assert!(response.headers().is_empty());
438
439        let response = Response::ok(()).with_header("x-ok", "line\nbreak");
440        assert!(response.headers().is_empty());
441    }
442
443    #[test]
444    fn cookies_stack_up_as_separate_headers() {
445        let response = Response::ok(())
446            .with_cookie(&Cookie::new("a", "1"))
447            .with_cookie(&Cookie::new("b", "2"))
448            .without_cookie("c");
449
450        assert_eq!(response.headers().get_all("set-cookie").iter().count(), 3);
451    }
452
453    #[test]
454    fn downloads_escape_the_file_name() {
455        let response = Response::download("data", "re\"port\r\n.csv");
456        assert_eq!(
457            response.header("content-disposition"),
458            Some("attachment; filename=\"report.csv\"")
459        );
460    }
461
462    #[tokio::test]
463    async fn into_response_covers_the_handler_return_types() {
464        assert_eq!(().into_response().status(), StatusCode::NO_CONTENT);
465        assert_eq!("hi".into_response().status(), StatusCode::OK);
466        assert_eq!(String::from("hi").into_response().status(), StatusCode::OK);
467        assert_eq!(StatusCode::IM_A_TEAPOT.into_response().status(), StatusCode::IM_A_TEAPOT);
468
469        let tuple = (StatusCode::ACCEPTED, "queued").into_response();
470        assert_eq!(tuple.status(), StatusCode::ACCEPTED);
471        assert_eq!(body_of(tuple).await, "queued");
472
473        let json = Json(json!({ "ok": true })).into_response();
474        assert_eq!(body_of(json).await, r#"{"ok":true}"#);
475
476        let html = Html("<b>x</b>").into_response();
477        assert_eq!(html.header("content-type"), Some("text/html; charset=utf-8"));
478    }
479
480    #[test]
481    fn options_and_results_carry_their_failure() {
482        let none: Option<&'static str> = None;
483        assert_eq!(none.into_response().status(), StatusCode::NOT_FOUND);
484        assert_eq!(Some("x").into_response().status(), StatusCode::OK);
485
486        let ok: Result<&'static str, Error> = Ok("x");
487        assert_eq!(ok.into_response().status(), StatusCode::OK);
488
489        let err: Result<&'static str, Error> = Err(Error::not_found("no post"));
490        assert_eq!(err.into_response().status(), StatusCode::NOT_FOUND);
491    }
492
493    #[tokio::test]
494    async fn errors_render_as_json_with_their_details() {
495        let error = Error::validation(json!({ "email": ["is required"] }));
496        let response = error.into_response();
497
498        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
499        let body: serde_json::Value = serde_json::from_str(&body_of(response).await).unwrap();
500        assert_eq!(body["message"], "The given data was invalid.");
501        assert_eq!(body["errors"]["email"][0], "is required");
502    }
503
504    #[test]
505    fn redirects_set_the_location_and_status() {
506        assert_eq!(Redirect::to("/a").into_response().status(), StatusCode::FOUND);
507        assert_eq!(
508            Redirect::permanent("/a").into_response().status(),
509            StatusCode::MOVED_PERMANENTLY
510        );
511        assert_eq!(Redirect::see_other("/a").into_response().status(), StatusCode::SEE_OTHER);
512        assert_eq!(
513            Redirect::temporary("/a").into_response().status(),
514            StatusCode::TEMPORARY_REDIRECT
515        );
516        assert_eq!(Redirect::to("/dash").into_response().header("location"), Some("/dash"));
517    }
518
519    #[tokio::test]
520    async fn converts_into_an_http_response() {
521        let response = Response::text("body").with_header("x-a", "1").into_http();
522        assert_eq!(response.status(), StatusCode::OK);
523        assert_eq!(response.headers().get("x-a").unwrap(), "1");
524    }
525}