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    /// The body, collected into bytes.
185    ///
186    /// Consumes the response, because a streaming body can only be read once.
187    /// For a test, [`into_string`](Self::into_string) is usually what you want.
188    pub async fn into_bytes(self) -> Result<Bytes, Error> {
189        self.body.collect().await
190    }
191
192    /// The body, collected and decoded as UTF-8.
193    ///
194    /// The one an assertion reaches for. Without it, reading a body in a test
195    /// means `response.into_http().into_body().collect().await` — correct for a
196    /// server, wrong ergonomics for `assert_eq!`.
197    ///
198    /// Invalid UTF-8 is replaced rather than refused: a test that is about to
199    /// print this wants to see what arrived, not an error about it.
200    pub async fn into_string(self) -> Result<String, Error> {
201        Ok(String::from_utf8_lossy(&self.into_bytes().await?).into_owned())
202    }
203
204    /// The body, parsed as JSON.
205    pub async fn into_json<T: serde::de::DeserializeOwned>(self) -> Result<T, Error> {
206        let bytes = self.into_bytes().await?;
207
208        serde_json::from_slice(&bytes).map_err(|e| {
209            // Quote what actually arrived: a JSON parse failure in a test is
210            // nearly always an error response nobody expected, and the message
211            // is the fastest way to see that.
212            let body = String::from_utf8_lossy(&bytes);
213            Error::internal(format!(
214                "the response body is not the JSON that was expected: {e} — body was: {}",
215                body.chars().take(400).collect::<String>()
216            ))
217        })
218    }
219
220    /// Convert into an `http::Response`, ready for the server to write.
221    pub fn into_http(self) -> http::Response<Body> {
222        let mut builder = http::Response::builder().status(self.status);
223        if let Some(headers) = builder.headers_mut() {
224            *headers = self.headers;
225        }
226        builder.body(self.body).unwrap_or_else(|_| {
227            // Unreachable: the status came from a `StatusCode` and the headers
228            // from a `HeaderMap`, both already validated.
229            http::Response::new(Body::Empty)
230        })
231    }
232}
233
234impl Default for Response {
235    fn default() -> Self {
236        Self::new(StatusCode::OK)
237    }
238}
239
240/// A redirect response.
241#[derive(Debug, Clone)]
242pub struct Redirect {
243    status: StatusCode,
244    location: String,
245}
246
247impl Redirect {
248    /// `302 Found` — a temporary redirect that clients may re-issue as GET.
249    pub fn to(location: impl Into<String>) -> Self {
250        Self { status: StatusCode::FOUND, location: location.into() }
251    }
252
253    /// `301 Moved Permanently`.
254    pub fn permanent(location: impl Into<String>) -> Self {
255        Self { status: StatusCode::MOVED_PERMANENTLY, location: location.into() }
256    }
257
258    /// `303 See Other` — the correct redirect after a successful POST, since
259    /// it tells the client to follow up with a GET.
260    pub fn see_other(location: impl Into<String>) -> Self {
261        Self { status: StatusCode::SEE_OTHER, location: location.into() }
262    }
263
264    /// `307 Temporary Redirect` — like 302 but the method is preserved.
265    pub fn temporary(location: impl Into<String>) -> Self {
266        Self { status: StatusCode::TEMPORARY_REDIRECT, location: location.into() }
267    }
268}
269
270/// A `text/html` body.
271#[derive(Debug, Clone)]
272pub struct Html<T>(pub T);
273
274/// An `application/json` body.
275#[derive(Debug, Clone)]
276pub struct Json<T>(pub T);
277
278/// Anything a handler can return.
279pub trait IntoResponse {
280    /// Build the response.
281    fn into_response(self) -> Response;
282}
283
284impl IntoResponse for Response {
285    fn into_response(self) -> Response {
286        self
287    }
288}
289
290impl IntoResponse for () {
291    fn into_response(self) -> Response {
292        Response::no_content()
293    }
294}
295
296impl IntoResponse for StatusCode {
297    fn into_response(self) -> Response {
298        Response::new(self)
299    }
300}
301
302impl IntoResponse for String {
303    fn into_response(self) -> Response {
304        Response::text(self)
305    }
306}
307
308impl IntoResponse for &'static str {
309    fn into_response(self) -> Response {
310        Response::text(self)
311    }
312}
313
314impl IntoResponse for serde_json::Value {
315    fn into_response(self) -> Response {
316        Response::json(&self)
317    }
318}
319
320impl<T: Serialize> IntoResponse for Json<T> {
321    fn into_response(self) -> Response {
322        Response::json(&self.0)
323    }
324}
325
326impl<T: Into<String>> IntoResponse for Html<T> {
327    fn into_response(self) -> Response {
328        Response::html(self.0)
329    }
330}
331
332impl IntoResponse for Redirect {
333    fn into_response(self) -> Response {
334        Response::new(self.status).with_header("location", &self.location)
335    }
336}
337
338impl IntoResponse for Body {
339    fn into_response(self) -> Response {
340        Response::ok(self)
341    }
342}
343
344impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
345    fn into_response(self) -> Response {
346        let (status, inner) = self;
347        inner.into_response().with_status(status)
348    }
349}
350
351impl<T: IntoResponse> IntoResponse for Option<T> {
352    /// `None` becomes a `404` — the usual meaning of "the handler found
353    /// nothing".
354    fn into_response(self) -> Response {
355        match self {
356            Some(inner) => inner.into_response(),
357            None => Error::not_found("Not Found").into_response(),
358        }
359    }
360}
361
362impl<T, E> IntoResponse for Result<T, E>
363where
364    T: IntoResponse,
365    E: IntoResponse,
366{
367    fn into_response(self) -> Response {
368        match self {
369            Ok(inner) => inner.into_response(),
370            Err(e) => e.into_response(),
371        }
372    }
373}
374
375/// Marks a response as having been rendered from a framework [`Error`], and
376/// carries the parts needed to render it *differently*.
377///
378/// Without this, the HTTP kernel could not tell a JSON error body it produced
379/// from one a handler deliberately returned, and so could not offer a browser
380/// an HTML error page instead. Attached to the response's extensions, so it
381/// never reaches the client.
382#[derive(Debug, Clone)]
383pub struct RenderedError {
384    /// The status the error rendered as.
385    pub status: u16,
386    /// The human-readable message.
387    pub message: String,
388    /// The structured details, if any.
389    pub details: Option<serde_json::Value>,
390    /// Whether the message is safe to show a client.
391    ///
392    /// `false` for 5xx: an internal error's message routinely contains a
393    /// connection string, a file path or a query. The kernel substitutes a
394    /// generic message unless the application is in debug mode.
395    pub disclosable: bool,
396}
397
398/// Renders a framework error as JSON.
399///
400/// The default, and what an API client should get. The HTTP kernel wraps it
401/// with content negotiation — an HTML error page for a browser — using the
402/// [`RenderedError`] this attaches.
403impl IntoResponse for Error {
404    fn into_response(self) -> Response {
405        let status =
406            StatusCode::from_u16(self.status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
407        let disclosable = !self.kind().is_server_error();
408
409        let mut payload = serde_json::Map::new();
410        payload.insert("message".into(), serde_json::Value::String(self.message().to_string()));
411        if let Some(details) = self.details() {
412            payload.insert("errors".into(), details.clone());
413        }
414
415        let mut response = Response::json(&serde_json::Value::Object(payload)).with_status(status);
416
417        response.extensions_mut().insert(RenderedError {
418            status: self.status(),
419            message: self.message().to_string(),
420            details: self.details().cloned(),
421            disclosable,
422        });
423        response
424    }
425}
426
427impl From<Error> for Response {
428    fn from(error: Error) -> Self {
429        error.into_response()
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use serde_json::json;
437
438    async fn body_of(response: Response) -> String {
439        String::from_utf8(response.body.collect().await.unwrap().to_vec()).unwrap()
440    }
441
442    #[tokio::test]
443    async fn builds_the_common_shapes() {
444        let text = Response::text("hi");
445        assert_eq!(text.status(), StatusCode::OK);
446        assert_eq!(text.header("content-type"), Some("text/plain; charset=utf-8"));
447        assert_eq!(body_of(text).await, "hi");
448
449        let html = Response::html("<p>hi</p>");
450        assert_eq!(html.header("content-type"), Some("text/html; charset=utf-8"));
451
452        let json = Response::json(&json!({ "a": 1 }));
453        assert_eq!(json.header("content-type"), Some("application/json; charset=utf-8"));
454        assert_eq!(body_of(json).await, r#"{"a":1}"#);
455
456        assert_eq!(Response::no_content().status(), StatusCode::NO_CONTENT);
457        assert_eq!(Response::created("/posts/1").header("location"), Some("/posts/1"));
458    }
459
460    #[test]
461    fn headers_replace_or_append() {
462        let response = Response::ok(()).with_header("x-a", "1").with_header("x-a", "2");
463        assert_eq!(response.headers().get_all("x-a").iter().count(), 1);
464        assert_eq!(response.header("x-a"), Some("2"));
465
466        let response = Response::ok(()).with_added_header("x-b", "1").with_added_header("x-b", "2");
467        assert_eq!(response.headers().get_all("x-b").iter().count(), 2);
468    }
469
470    #[test]
471    fn an_invalid_header_is_dropped_rather_than_panicking() {
472        let response = Response::ok(()).with_header("bad header name", "v");
473        assert!(response.headers().is_empty());
474
475        let response = Response::ok(()).with_header("x-ok", "line\nbreak");
476        assert!(response.headers().is_empty());
477    }
478
479    #[test]
480    fn cookies_stack_up_as_separate_headers() {
481        let response = Response::ok(())
482            .with_cookie(&Cookie::new("a", "1"))
483            .with_cookie(&Cookie::new("b", "2"))
484            .without_cookie("c");
485
486        assert_eq!(response.headers().get_all("set-cookie").iter().count(), 3);
487    }
488
489    #[test]
490    fn downloads_escape_the_file_name() {
491        let response = Response::download("data", "re\"port\r\n.csv");
492        assert_eq!(
493            response.header("content-disposition"),
494            Some("attachment; filename=\"report.csv\"")
495        );
496    }
497
498    #[tokio::test]
499    async fn into_response_covers_the_handler_return_types() {
500        assert_eq!(().into_response().status(), StatusCode::NO_CONTENT);
501        assert_eq!("hi".into_response().status(), StatusCode::OK);
502        assert_eq!(String::from("hi").into_response().status(), StatusCode::OK);
503        assert_eq!(StatusCode::IM_A_TEAPOT.into_response().status(), StatusCode::IM_A_TEAPOT);
504
505        let tuple = (StatusCode::ACCEPTED, "queued").into_response();
506        assert_eq!(tuple.status(), StatusCode::ACCEPTED);
507        assert_eq!(body_of(tuple).await, "queued");
508
509        let json = Json(json!({ "ok": true })).into_response();
510        assert_eq!(body_of(json).await, r#"{"ok":true}"#);
511
512        let html = Html("<b>x</b>").into_response();
513        assert_eq!(html.header("content-type"), Some("text/html; charset=utf-8"));
514    }
515
516    #[test]
517    fn options_and_results_carry_their_failure() {
518        let none: Option<&'static str> = None;
519        assert_eq!(none.into_response().status(), StatusCode::NOT_FOUND);
520        assert_eq!(Some("x").into_response().status(), StatusCode::OK);
521
522        let ok: Result<&'static str, Error> = Ok("x");
523        assert_eq!(ok.into_response().status(), StatusCode::OK);
524
525        let err: Result<&'static str, Error> = Err(Error::not_found("no post"));
526        assert_eq!(err.into_response().status(), StatusCode::NOT_FOUND);
527    }
528
529    #[tokio::test]
530    async fn errors_render_as_json_with_their_details() {
531        let error = Error::validation(json!({ "email": ["is required"] }));
532        let response = error.into_response();
533
534        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
535        let body: serde_json::Value = serde_json::from_str(&body_of(response).await).unwrap();
536        assert_eq!(body["message"], "The given data was invalid.");
537        assert_eq!(body["errors"]["email"][0], "is required");
538    }
539
540    #[test]
541    fn redirects_set_the_location_and_status() {
542        assert_eq!(Redirect::to("/a").into_response().status(), StatusCode::FOUND);
543        assert_eq!(
544            Redirect::permanent("/a").into_response().status(),
545            StatusCode::MOVED_PERMANENTLY
546        );
547        assert_eq!(Redirect::see_other("/a").into_response().status(), StatusCode::SEE_OTHER);
548        assert_eq!(
549            Redirect::temporary("/a").into_response().status(),
550            StatusCode::TEMPORARY_REDIRECT
551        );
552        assert_eq!(Redirect::to("/dash").into_response().header("location"), Some("/dash"));
553    }
554
555    #[tokio::test]
556    async fn converts_into_an_http_response() {
557        let response = Response::text("body").with_header("x-a", "1").into_http();
558        assert_eq!(response.status(), StatusCode::OK);
559        assert_eq!(response.headers().get("x-a").unwrap(), "1");
560    }
561}