Skip to main content

actix_cloud/
response.rs

1//! Provide useful response types.
2//!
3//! With the `i18n` feature, response messages are translated automatically through the
4//! request locale; with the `response-json` feature, [`JsonResponse`] serializes the
5//! body as `{"code": ..., "message": ..., "data": ...}`. To define response codes in
6//! YAML files and generate the corresponding enums, see
7//! [`response_build`](crate::response_build).
8use std::fmt::{self, Display};
9
10use actix_web::{
11    http::{
12        header::{self, ContentDisposition, DispositionParam, DispositionType},
13        StatusCode,
14    },
15    HttpResponse, HttpResponseBuilder,
16};
17use futures::{future, stream::once};
18
19/// Alias for a `Result` with [`ResponseError`] as its error type.
20pub type RspResult<T> = Result<T, ResponseError>;
21
22/// Error type for handlers returning [`RspResult`].
23///
24/// Wraps an `anyhow::Error`; the HTTP response is always an opaque 500 without
25/// leaking the error details to the client.
26#[derive(Debug)]
27pub struct ResponseError(anyhow::Error);
28
29impl Display for ResponseError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        f.write_str(&self.0.to_string())
32    }
33}
34
35impl actix_web::ResponseError for ResponseError {
36    fn status_code(&self) -> StatusCode {
37        StatusCode::INTERNAL_SERVER_ERROR
38    }
39
40    fn error_response(&self) -> HttpResponse {
41        HttpResponse::build(self.status_code()).finish()
42    }
43}
44
45impl<T> From<T> for ResponseError
46where
47    T: Into<anyhow::Error>,
48{
49    fn from(t: T) -> Self {
50        Self(t.into())
51    }
52}
53
54/// Business code and message carried by a [`Response`].
55///
56/// Implemented by the enums generated by
57/// [`generate_response`](crate::response_build::generate_response), or by hand.
58pub trait ResponseCodeTrait {
59    /// Business code in the response body.
60    fn code(&self) -> i64;
61    /// Message in the response body (an i18n key when the `i18n` feature is enabled).
62    fn message(&self) -> &'static str;
63}
64
65pub type ResponseBuilderFn = Box<dyn Fn(&mut HttpResponseBuilder)>;
66
67/// Uniform response type with business code/message/data and i18n support.
68///
69/// Build one from a [`ResponseCodeTrait`] (via [`Self::new`]) or straight from an HTTP
70/// [`StatusCode`] (via [`Self::new_code`]); finalize it through the builder-style methods
71/// ([`message`](Self::message), [`data`](Self::data), [`builder`](Self::builder), ...).
72/// `Response<serde_json::Value>` (aka [`JsonResponse`], feature `response-json`)
73/// implements [`Responder`](actix_web::Responder) and serializes the body as JSON.
74pub struct Response<T> {
75    pub http_code: StatusCode,
76    pub code: i64,
77    pub message: String,
78    pub data: Option<T>,
79    pub builder: Vec<ResponseBuilderFn>,
80    #[cfg(feature = "i18n")]
81    pub translate: bool,
82}
83
84impl<T> Response<T> {
85    /// Create a 200 response from a [`ResponseCodeTrait`], with i18n translation enabled
86    /// for the message (feature `i18n`).
87    pub fn new<C>(r: C) -> Self
88    where
89        C: ResponseCodeTrait,
90    {
91        Self {
92            http_code: StatusCode::OK,
93            code: r.code(),
94            message: r.message().to_owned(),
95            data: None,
96            builder: Vec::new(),
97            #[cfg(feature = "i18n")]
98            translate: true,
99        }
100    }
101
102    /// Create a response from a raw HTTP status code, with an empty message and
103    /// i18n translation disabled.
104    pub fn new_code(code: StatusCode) -> Self {
105        Self {
106            http_code: code,
107            code: 0,
108            message: String::new(),
109            data: None,
110            builder: Vec::new(),
111            #[cfg(feature = "i18n")]
112            translate: false,
113        }
114    }
115
116    /// Create a 201 Created response with a `Location` header pointing to `location`.
117    pub fn created<C, S>(r: C, location: S) -> Self
118    where
119        C: ResponseCodeTrait,
120        S: Into<String>,
121    {
122        let mut ret = Self::new(r);
123        ret.http_code = StatusCode::CREATED;
124        let location: String = location.into();
125        ret.builder(move |r| {
126            r.insert_header((header::LOCATION, location.clone()));
127        })
128    }
129
130    /// Create a 204 No Content response (empty body, no JSON headers).
131    pub fn no_content() -> Self {
132        Self::new_code(StatusCode::NO_CONTENT)
133    }
134
135    /// Create a 400 Bad Request response with `s` as the message.
136    pub fn bad_request<S: Into<String>>(s: S) -> Self {
137        Self::new_code(StatusCode::BAD_REQUEST).message(s)
138    }
139
140    /// Create a 403 Forbidden response.
141    pub fn forbidden() -> Self {
142        Self::new_code(StatusCode::FORBIDDEN)
143    }
144
145    /// Create a 404 Not Found response.
146    pub fn not_found() -> Self {
147        Self::new_code(StatusCode::NOT_FOUND)
148    }
149
150    /// Create a redirect response (`code` should be a 3xx status) with a
151    /// `Location` header pointing to `s`.
152    pub fn redirect<S: Into<String>>(code: StatusCode, s: S) -> Self {
153        let s: String = s.into();
154        Self::new_code(code).builder(move |r| {
155            r.insert_header((header::LOCATION, s.clone()));
156        })
157    }
158
159    /// Add a callback to mutate the `HttpResponseBuilder` before the response is built
160    /// (e.g. to insert extra headers). Multiple callbacks are applied in call order.
161    pub fn builder<F>(mut self, f: F) -> Self
162    where
163        F: Fn(&mut HttpResponseBuilder) + 'static,
164    {
165        self.builder.push(Box::new(f));
166        self
167    }
168
169    /// Override the response message. With feature `i18n` the message is treated as a
170    /// translation key (see [`Self::translate`]).
171    pub fn message<S: Into<String>>(mut self, s: S) -> Self {
172        self.message = s.into();
173        self
174    }
175
176    /// Attach the response payload.
177    pub fn data(mut self, data: T) -> Self {
178        self.data = Some(data);
179        self
180    }
181
182    /// Build a file-download response: `data` is sent as an attachment named `name`
183    /// (`application/octet-stream`).
184    pub fn file(name: String, data: Vec<u8>) -> HttpResponse {
185        let body = once(future::ok::<_, actix_web::Error>(data.into()));
186        let header = ContentDisposition {
187            disposition: DispositionType::Attachment,
188            parameters: vec![DispositionParam::Filename(name)],
189        };
190        HttpResponse::Ok()
191            .insert_header(("Content-Disposition", header))
192            .content_type("application/octet-stream")
193            .streaming(body)
194    }
195
196    #[cfg(feature = "i18n")]
197    /// Enable i18n translation of the message (feature `i18n`).
198    ///
199    /// Enabled by default for [`Self::new`], disabled for [`Self::new_code`].
200    pub fn translate(mut self) -> Self {
201        self.translate = true;
202        self
203    }
204
205    #[cfg(feature = "i18n")]
206    /// Translate the message with the request locale (from [`crate::request::Extension`],
207    /// falling back to `locale.default` in `GlobalState`).
208    ///
209    /// Returns the message as-is when translation is disabled, when `GlobalState` is not
210    /// registered, or when the request extension is missing.
211    pub fn i18n_message(&self, req: &actix_web::HttpRequest) -> String {
212        use actix_web::HttpMessage as _;
213
214        if self.translate {
215            req.app_data::<actix_web::web::Data<crate::state::GlobalState>>()
216                .map_or_else(
217                    || self.message.clone(),
218                    |state| {
219                        if let Some(ext) = req
220                            .extensions()
221                            .get::<std::sync::Arc<crate::request::Extension>>()
222                        {
223                            crate::t!(state.locale, &self.message, &ext.lang)
224                        } else {
225                            self.message.clone()
226                        }
227                    },
228                )
229        } else {
230            self.message.clone()
231        }
232    }
233}
234
235#[cfg(feature = "response-json")]
236pub type JsonResponse = Response<serde_json::Value>;
237
238#[cfg(feature = "response-json")]
239impl JsonResponse {
240    /// Attach `data`, serialized to JSON, as the response payload.
241    pub fn json<T: serde::Serialize>(mut self, data: T) -> Self {
242        self.data = Some(serde_json::json!(data));
243        self
244    }
245}
246
247#[cfg(feature = "response-json")]
248impl actix_web::Responder for JsonResponse {
249    type Body = actix_web::body::EitherBody<String>;
250
251    fn respond_to(
252        self,
253        #[allow(unused_variables)] req: &actix_web::HttpRequest,
254    ) -> HttpResponse<Self::Body> {
255        if self.http_code.is_success() {
256            #[cfg(feature = "i18n")]
257            let message = self.i18n_message(req);
258            #[cfg(not(feature = "i18n"))]
259            let message = self.message;
260            let mut body = serde_json::json!({
261                "code": self.code,
262                "message": message,
263            });
264            if let Some(data) = self.data {
265                body.as_object_mut()
266                    .unwrap()
267                    .insert(String::from("data"), data);
268            }
269            let body = body.to_string();
270            let mut rsp = HttpResponse::build(self.http_code);
271            if self.http_code != StatusCode::NO_CONTENT {
272                rsp.content_type(actix_web::http::header::ContentType::json());
273            }
274            for builder in self.builder {
275                builder(&mut rsp);
276            }
277            if self.http_code == StatusCode::NO_CONTENT {
278                rsp.finish().map_into_right_body()
279            } else {
280                rsp.message_body(body).unwrap().map_into_left_body()
281            }
282        } else {
283            let mut rsp = HttpResponse::build(self.http_code);
284            for builder in self.builder {
285                builder(&mut rsp);
286            }
287            if self.message.is_empty() {
288                rsp.finish().map_into_right_body()
289            } else {
290                rsp.message_body(self.message).unwrap().map_into_left_body()
291            }
292        }
293    }
294}