actix_web/response/
builder.rs

1use std::{
2    cell::{Ref, RefMut},
3    future::Future,
4    pin::Pin,
5    task::{Context, Poll},
6};
7
8use actix_http::{error::HttpError, Response, ResponseHead};
9use bytes::Bytes;
10use futures_core::Stream;
11use serde::Serialize;
12
13use crate::{
14    body::{BodyStream, BoxBody, MessageBody, SizedStream},
15    dev::Extensions,
16    error::{Error, JsonPayloadError},
17    http::{
18        header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue},
19        ConnectionType, StatusCode,
20    },
21    BoxError, HttpRequest, HttpResponse, Responder,
22};
23
24/// An HTTP response builder.
25///
26/// This type can be used to construct an instance of `Response` through a builder-like pattern.
27pub struct HttpResponseBuilder {
28    res: Option<Response<BoxBody>>,
29    error: Option<HttpError>,
30}
31
32impl HttpResponseBuilder {
33    #[inline]
34    /// Create response builder
35    pub fn new(status: StatusCode) -> Self {
36        Self {
37            res: Some(Response::with_body(status, BoxBody::new(()))),
38            error: None,
39        }
40    }
41
42    /// Set HTTP status code of this response.
43    #[inline]
44    pub fn status(&mut self, status: StatusCode) -> &mut Self {
45        if let Some(parts) = self.inner() {
46            parts.status = status;
47        }
48        self
49    }
50
51    /// Insert a header, replacing any that were set with an equivalent field name.
52    ///
53    /// ```
54    /// use actix_web::{HttpResponse, http::header};
55    ///
56    /// HttpResponse::Ok()
57    ///     .insert_header(header::ContentType(mime::APPLICATION_JSON))
58    ///     .insert_header(("X-TEST", "value"))
59    ///     .finish();
60    /// ```
61    pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
62        if let Some(parts) = self.inner() {
63            match header.try_into_pair() {
64                Ok((key, value)) => {
65                    parts.headers.insert(key, value);
66                }
67                Err(err) => self.error = Some(err.into()),
68            };
69        }
70
71        self
72    }
73
74    /// Append a header, keeping any that were set with an equivalent field name.
75    ///
76    /// ```
77    /// use actix_web::{HttpResponse, http::header};
78    ///
79    /// HttpResponse::Ok()
80    ///     .append_header(header::ContentType(mime::APPLICATION_JSON))
81    ///     .append_header(("X-TEST", "value1"))
82    ///     .append_header(("X-TEST", "value2"))
83    ///     .finish();
84    /// ```
85    pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
86        if let Some(parts) = self.inner() {
87            match header.try_into_pair() {
88                Ok((key, value)) => parts.headers.append(key, value),
89                Err(err) => self.error = Some(err.into()),
90            };
91        }
92
93        self
94    }
95
96    /// Replaced with [`Self::insert_header()`].
97    #[doc(hidden)]
98    #[deprecated(
99        since = "4.0.0",
100        note = "Replaced with `insert_header((key, value))`. Will be removed in v5."
101    )]
102    pub fn set_header<K, V>(&mut self, key: K, value: V) -> &mut Self
103    where
104        K: TryInto<HeaderName>,
105        K::Error: Into<HttpError>,
106        V: TryIntoHeaderValue,
107    {
108        if self.error.is_some() {
109            return self;
110        }
111
112        match (key.try_into(), value.try_into_value()) {
113            (Ok(name), Ok(value)) => return self.insert_header((name, value)),
114            (Err(err), _) => self.error = Some(err.into()),
115            (_, Err(err)) => self.error = Some(err.into()),
116        }
117
118        self
119    }
120
121    /// Replaced with [`Self::append_header()`].
122    #[doc(hidden)]
123    #[deprecated(
124        since = "4.0.0",
125        note = "Replaced with `append_header((key, value))`. Will be removed in v5."
126    )]
127    pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
128    where
129        K: TryInto<HeaderName>,
130        K::Error: Into<HttpError>,
131        V: TryIntoHeaderValue,
132    {
133        if self.error.is_some() {
134            return self;
135        }
136
137        match (key.try_into(), value.try_into_value()) {
138            (Ok(name), Ok(value)) => return self.append_header((name, value)),
139            (Err(err), _) => self.error = Some(err.into()),
140            (_, Err(err)) => self.error = Some(err.into()),
141        }
142
143        self
144    }
145
146    /// Set the custom reason for the response.
147    #[inline]
148    pub fn reason(&mut self, reason: &'static str) -> &mut Self {
149        if let Some(parts) = self.inner() {
150            parts.reason = Some(reason);
151        }
152        self
153    }
154
155    /// Set connection type to KeepAlive
156    #[inline]
157    pub fn keep_alive(&mut self) -> &mut Self {
158        if let Some(parts) = self.inner() {
159            parts.set_connection_type(ConnectionType::KeepAlive);
160        }
161        self
162    }
163
164    /// Set connection type to Upgrade
165    #[inline]
166    pub fn upgrade<V>(&mut self, value: V) -> &mut Self
167    where
168        V: TryIntoHeaderValue,
169    {
170        if let Some(parts) = self.inner() {
171            parts.set_connection_type(ConnectionType::Upgrade);
172        }
173
174        if let Ok(value) = value.try_into_value() {
175            self.insert_header((header::UPGRADE, value));
176        }
177
178        self
179    }
180
181    /// Force close connection, even if it is marked as keep-alive
182    #[inline]
183    pub fn force_close(&mut self) -> &mut Self {
184        if let Some(parts) = self.inner() {
185            parts.set_connection_type(ConnectionType::Close);
186        }
187        self
188    }
189
190    /// Disable chunked transfer encoding for HTTP/1.1 streaming responses.
191    #[inline]
192    pub fn no_chunking(&mut self, len: u64) -> &mut Self {
193        let mut buf = itoa::Buffer::new();
194        self.insert_header((header::CONTENT_LENGTH, buf.format(len)));
195
196        if let Some(parts) = self.inner() {
197            parts.no_chunking(true);
198        }
199        self
200    }
201
202    /// Set response content type.
203    #[inline]
204    pub fn content_type<V>(&mut self, value: V) -> &mut Self
205    where
206        V: TryIntoHeaderValue,
207    {
208        if let Some(parts) = self.inner() {
209            match value.try_into_value() {
210                Ok(value) => {
211                    parts.headers.insert(header::CONTENT_TYPE, value);
212                }
213                Err(err) => self.error = Some(err.into()),
214            };
215        }
216        self
217    }
218
219    /// Add a cookie to the response.
220    ///
221    /// To send a "removal" cookie, call [`.make_removal()`](cookie::Cookie::make_removal) on the
222    /// given cookie. See [`HttpResponse::add_removal_cookie()`] to learn more.
223    ///
224    /// # Examples
225    /// Send a new cookie:
226    /// ```
227    /// use actix_web::{HttpResponse, cookie::Cookie};
228    ///
229    /// let res = HttpResponse::Ok()
230    ///     .cookie(
231    ///         Cookie::build("name", "value")
232    ///             .domain("www.rust-lang.org")
233    ///             .path("/")
234    ///             .secure(true)
235    ///             .http_only(true)
236    ///             .finish(),
237    ///     )
238    ///     .finish();
239    /// ```
240    ///
241    /// Send a removal cookie:
242    /// ```
243    /// use actix_web::{HttpResponse, cookie::Cookie};
244    ///
245    /// // the name, domain and path match the cookie created in the previous example
246    /// let mut cookie = Cookie::build("name", "value-does-not-matter")
247    ///     .domain("www.rust-lang.org")
248    ///     .path("/")
249    ///     .finish();
250    /// cookie.make_removal();
251    ///
252    /// let res = HttpResponse::Ok()
253    ///     .cookie(cookie)
254    ///     .finish();
255    /// ```
256    #[cfg(feature = "cookies")]
257    pub fn cookie(&mut self, cookie: cookie::Cookie<'_>) -> &mut Self {
258        match cookie.to_string().try_into_value() {
259            Ok(hdr_val) => self.append_header((header::SET_COOKIE, hdr_val)),
260            Err(err) => {
261                self.error = Some(err.into());
262                self
263            }
264        }
265    }
266
267    /// Returns a reference to the response-local data/extensions container.
268    #[inline]
269    pub fn extensions(&self) -> Ref<'_, Extensions> {
270        self.res
271            .as_ref()
272            .expect("cannot reuse response builder")
273            .extensions()
274    }
275
276    /// Returns a mutable reference to the response-local data/extensions container.
277    #[inline]
278    pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
279        self.res
280            .as_mut()
281            .expect("cannot reuse response builder")
282            .extensions_mut()
283    }
284
285    /// Set a body and build the `HttpResponse`.
286    ///
287    /// Unlike [`message_body`](Self::message_body), errors are converted into error
288    /// responses immediately.
289    ///
290    /// `HttpResponseBuilder` can not be used after this call.
291    pub fn body<B>(&mut self, body: B) -> HttpResponse<BoxBody>
292    where
293        B: MessageBody + 'static,
294    {
295        match self.message_body(body) {
296            Ok(res) => res.map_into_boxed_body(),
297            Err(err) => HttpResponse::from_error(err),
298        }
299    }
300
301    /// Set a body and build the `HttpResponse`.
302    ///
303    /// `HttpResponseBuilder` can not be used after this call.
304    pub fn message_body<B>(&mut self, body: B) -> Result<HttpResponse<B>, Error> {
305        if let Some(err) = self.error.take() {
306            return Err(err.into());
307        }
308
309        let res = self
310            .res
311            .take()
312            .expect("cannot reuse response builder")
313            .set_body(body);
314
315        Ok(HttpResponse::from(res))
316    }
317
318    /// Set a streaming body and build the `HttpResponse`.
319    ///
320    /// `HttpResponseBuilder` can not be used after this call.
321    ///
322    /// If `Content-Type` is not set, then it is automatically set to `application/octet-stream`.
323    ///
324    /// If `Content-Length` is set, then [`no_chunking()`](Self::no_chunking) is automatically called.
325    #[inline]
326    pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
327    where
328        S: Stream<Item = Result<Bytes, E>> + 'static,
329        E: Into<BoxError> + 'static,
330    {
331        // Set mime type to application/octet-stream if it is not set
332        if let Some(parts) = self.inner() {
333            if !parts.headers.contains_key(header::CONTENT_TYPE) {
334                self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
335            }
336        }
337
338        let content_length = self
339            .inner()
340            .and_then(|parts| parts.headers.get(header::CONTENT_LENGTH))
341            .and_then(|value| value.to_str().ok())
342            .and_then(|value| value.parse::<u64>().ok());
343
344        if let Some(len) = content_length {
345            self.no_chunking(len);
346            self.body(SizedStream::new(len, stream))
347        } else {
348            self.body(BodyStream::new(stream))
349        }
350    }
351
352    /// Set a JSON body and build the `HttpResponse`.
353    ///
354    /// `HttpResponseBuilder` can not be used after this call.
355    pub fn json(&mut self, value: impl Serialize) -> HttpResponse {
356        match serde_json::to_string(&value) {
357            Ok(body) => {
358                let contains = if let Some(parts) = self.inner() {
359                    parts.headers.contains_key(header::CONTENT_TYPE)
360                } else {
361                    true
362                };
363
364                if !contains {
365                    self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON));
366                }
367
368                self.body(body)
369            }
370            Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)),
371        }
372    }
373
374    /// Set an empty body and build the `HttpResponse`.
375    ///
376    /// `HttpResponseBuilder` can not be used after this call.
377    #[inline]
378    pub fn finish(&mut self) -> HttpResponse {
379        self.body(())
380    }
381
382    /// This method construct new `HttpResponseBuilder`
383    pub fn take(&mut self) -> Self {
384        Self {
385            res: self.res.take(),
386            error: self.error.take(),
387        }
388    }
389
390    fn inner(&mut self) -> Option<&mut ResponseHead> {
391        if self.error.is_some() {
392            return None;
393        }
394
395        self.res.as_mut().map(Response::head_mut)
396    }
397}
398
399impl From<HttpResponseBuilder> for HttpResponse {
400    fn from(mut builder: HttpResponseBuilder) -> Self {
401        builder.finish()
402    }
403}
404
405impl From<HttpResponseBuilder> for Response<BoxBody> {
406    fn from(mut builder: HttpResponseBuilder) -> Self {
407        builder.finish().into()
408    }
409}
410
411impl Future for HttpResponseBuilder {
412    type Output = Result<HttpResponse, Error>;
413
414    fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
415        Poll::Ready(Ok(self.finish()))
416    }
417}
418
419impl Responder for HttpResponseBuilder {
420    type Body = BoxBody;
421
422    #[inline]
423    fn respond_to(mut self, _: &HttpRequest) -> HttpResponse<Self::Body> {
424        self.finish()
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use crate::{
432        body,
433        http::header::{HeaderValue, CONTENT_TYPE},
434        test::assert_body_eq,
435    };
436
437    #[test]
438    fn test_basic_builder() {
439        let resp = HttpResponse::Ok()
440            .insert_header(("X-TEST", "value"))
441            .finish();
442        assert_eq!(resp.status(), StatusCode::OK);
443    }
444
445    #[test]
446    fn test_upgrade() {
447        let resp = HttpResponseBuilder::new(StatusCode::OK)
448            .upgrade("websocket")
449            .finish();
450        assert!(resp.upgrade());
451        assert_eq!(
452            resp.headers().get(header::UPGRADE).unwrap(),
453            HeaderValue::from_static("websocket")
454        );
455    }
456
457    #[test]
458    fn test_force_close() {
459        let resp = HttpResponseBuilder::new(StatusCode::OK)
460            .force_close()
461            .finish();
462        assert!(!resp.keep_alive())
463    }
464
465    #[test]
466    fn test_content_type() {
467        let resp = HttpResponseBuilder::new(StatusCode::OK)
468            .content_type("text/plain")
469            .body(Bytes::new());
470        assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
471    }
472
473    #[actix_rt::test]
474    async fn test_json() {
475        let res = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]);
476        let ct = res.headers().get(CONTENT_TYPE).unwrap();
477        assert_eq!(ct, HeaderValue::from_static("application/json"));
478        assert_body_eq!(res, br#"["v1","v2","v3"]"#);
479
480        let res = HttpResponse::Ok().json(["v1", "v2", "v3"]);
481        let ct = res.headers().get(CONTENT_TYPE).unwrap();
482        assert_eq!(ct, HeaderValue::from_static("application/json"));
483        assert_body_eq!(res, br#"["v1","v2","v3"]"#);
484
485        // content type override
486        let res = HttpResponse::Ok()
487            .insert_header((CONTENT_TYPE, "text/json"))
488            .json(["v1", "v2", "v3"]);
489        let ct = res.headers().get(CONTENT_TYPE).unwrap();
490        assert_eq!(ct, HeaderValue::from_static("text/json"));
491        assert_body_eq!(res, br#"["v1","v2","v3"]"#);
492    }
493
494    #[actix_rt::test]
495    async fn test_serde_json_in_body() {
496        let resp = HttpResponse::Ok()
497            .body(serde_json::to_vec(&serde_json::json!({ "test-key": "test-value" })).unwrap());
498
499        assert_eq!(
500            body::to_bytes(resp.into_body()).await.unwrap().as_ref(),
501            br#"{"test-key":"test-value"}"#
502        );
503    }
504
505    #[test]
506    fn response_builder_header_insert_kv() {
507        let mut res = HttpResponse::Ok();
508        res.insert_header(("Content-Type", "application/octet-stream"));
509        let res = res.finish();
510
511        assert_eq!(
512            res.headers().get("Content-Type"),
513            Some(&HeaderValue::from_static("application/octet-stream"))
514        );
515    }
516
517    #[test]
518    fn response_builder_header_insert_typed() {
519        let mut res = HttpResponse::Ok();
520        res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
521        let res = res.finish();
522
523        assert_eq!(
524            res.headers().get("Content-Type"),
525            Some(&HeaderValue::from_static("application/octet-stream"))
526        );
527    }
528
529    #[test]
530    fn response_builder_header_append_kv() {
531        let mut res = HttpResponse::Ok();
532        res.append_header(("Content-Type", "application/octet-stream"));
533        res.append_header(("Content-Type", "application/json"));
534        let res = res.finish();
535
536        let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
537        assert_eq!(headers.len(), 2);
538        assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
539        assert!(headers.contains(&HeaderValue::from_static("application/json")));
540    }
541
542    #[test]
543    fn response_builder_header_append_typed() {
544        let mut res = HttpResponse::Ok();
545        res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
546        res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON));
547        let res = res.finish();
548
549        let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
550        assert_eq!(headers.len(), 2);
551        assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
552        assert!(headers.contains(&HeaderValue::from_static("application/json")));
553    }
554}