Skip to main content

ocpi_kit/server/
error.rs

1//! Turning an [`OcpiError`] into the HTTP response the specification prescribes.
2
3use axum::response::{IntoResponse, Response};
4use http::{HeaderMap, HeaderValue, StatusCode as HttpStatus};
5
6use crate::transport::headers::APPLICATION_JSON;
7use crate::transport::{OcpiError, OcpiResponse, RequestIds, RoutingHeaders};
8use crate::types::Validate;
9
10/// An OCPI response on its way out: the envelope, plus the headers that must accompany it.
11///
12/// Every OCPI response carries the request and correlation IDs it was called with:
13///
14/// > *Every request SHALL contain a unique request ID, the response to this request SHALL contain
15/// > the same ID.*
16///
17/// so this type carries them rather than leaving it to each handler to remember.
18#[derive(Debug)]
19pub struct OcpiReply<T> {
20    envelope: OcpiResponse<T>,
21    http_status: HttpStatus,
22    ids: Option<RequestIds>,
23    routing: Option<RoutingHeaders>,
24    extra: HeaderMap,
25}
26
27impl<T: Validate> OcpiReply<T> {
28    /// A `1000 Generic success` reply with a payload, answered with HTTP 200.
29    ///
30    /// The payload is validated on its way out, every violation is logged by JSON Pointer at
31    /// `warn`, and it is **sent anyway**. Refusing a partner's `GET` because one Location in a page
32    /// of a hundred has a 46-character name turns this party's data quality into an outage on
33    /// theirs; serving it silently is how it becomes their support ticket weeks later.
34    ///
35    /// A handler that would rather refuse calls
36    /// [`validate`](crate::types::Validate::validate) itself and returns
37    /// [`OcpiError::Invalid`](crate::transport::OcpiError::Invalid).
38    #[must_use]
39    pub fn ok(data: T) -> Self {
40        if let Err(violations) = data.validate() {
41            tracing::warn!(
42                ocpi.violations = violations.len(),
43                "this server is about to answer with an object that does not conform: {violations}",
44            );
45        }
46        Self {
47            envelope: OcpiResponse::success(data),
48            http_status: HttpStatus::OK,
49            ids: None,
50            routing: None,
51            extra: HeaderMap::new(),
52        }
53    }
54
55    /// A `1000` reply for an object that was newly created, answered with HTTP 201.
56    ///
57    /// > *HTTP `201 - Created` when the object has been newly created in the server system.*
58    #[must_use]
59    pub fn created(data: T) -> Self {
60        Self { http_status: HttpStatus::CREATED, ..Self::ok(data) }
61    }
62}
63
64impl<T> OcpiReply<T> {
65    /// A `1000` reply with no payload, for a PUT or PATCH whose response body is unspecified.
66    ///
67    /// > *We also advise that in such cases, platform sending the response leave the `data` field
68    /// > unset in the response format.*
69    #[must_use]
70    pub fn no_content() -> Self {
71        Self {
72            envelope: OcpiResponse::success_empty(),
73            http_status: HttpStatus::OK,
74            ids: None,
75            routing: None,
76            extra: HeaderMap::new(),
77        }
78    }
79
80    /// Attaches the request and correlation IDs of the request being answered.
81    #[must_use]
82    pub fn with_ids(mut self, ids: RequestIds) -> Self {
83        self.ids = Some(ids);
84        self
85    }
86
87    /// Attaches the routing headers of the response.
88    ///
89    /// Pass the headers already swapped for the response direction; see
90    /// [`RoutingHeaders::response_from`].
91    #[must_use]
92    pub fn with_routing(mut self, routing: RoutingHeaders) -> Self {
93        self.routing = Some(routing);
94        self
95    }
96
97    /// Adds arbitrary response headers, such as the pagination trio or a `Location`.
98    #[must_use]
99    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
100        self.extra.extend(headers);
101        self
102    }
103
104    /// Overrides the HTTP status.
105    #[must_use]
106    pub const fn with_http_status(mut self, status: HttpStatus) -> Self {
107        self.http_status = status;
108        self
109    }
110
111    /// Sets the `status_message`.
112    #[must_use]
113    pub fn with_message(mut self, message: impl Into<String>) -> Self {
114        self.envelope.status_message = Some(message.into());
115        self
116    }
117
118    /// The envelope that will be sent.
119    #[must_use]
120    pub const fn envelope(&self) -> &OcpiResponse<T> {
121        &self.envelope
122    }
123}
124
125impl<T: serde::Serialize> IntoResponse for OcpiReply<T> {
126    fn into_response(self) -> Response {
127        let mut headers = self.extra;
128        if let Some(ids) = &self.ids {
129            ids.write_to(&mut headers);
130        }
131        if let Some(routing) = &self.routing {
132            routing.write_to(&mut headers);
133        }
134        headers.insert(http::header::CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON));
135
136        let body = match serde_json::to_vec(&self.envelope) {
137            Ok(body) => body,
138            Err(e) => {
139                return internal_error(&format!("cannot serialise the response: {e}"), &headers);
140            }
141        };
142        (self.http_status, headers, body).into_response()
143    }
144}
145
146/// The HTTP response an [`OcpiError`] becomes.
147///
148/// The mapping is [`OcpiError::http_status`], which encodes the whole of §status_codes: only five
149/// situations get an HTTP error, and everything that reached the OCPI layer is a `200 OK` with a
150/// four-digit code in the body.
151#[derive(Debug)]
152pub struct OcpiErrorResponse {
153    error: OcpiError,
154    ids: Option<RequestIds>,
155}
156
157impl OcpiErrorResponse {
158    /// Wraps an error.
159    #[must_use]
160    pub const fn new(error: OcpiError) -> Self {
161        Self { error, ids: None }
162    }
163
164    /// Attaches the request and correlation IDs of the request being answered.
165    #[must_use]
166    pub fn with_ids(mut self, ids: RequestIds) -> Self {
167        self.ids = Some(ids);
168        self
169    }
170
171    /// The error being reported.
172    #[must_use]
173    pub const fn error(&self) -> &OcpiError {
174        &self.error
175    }
176}
177
178impl From<OcpiError> for OcpiErrorResponse {
179    fn from(error: OcpiError) -> Self {
180        Self::new(error)
181    }
182}
183
184impl IntoResponse for OcpiErrorResponse {
185    fn into_response(self) -> Response {
186        let mut headers = HeaderMap::new();
187        if let Some(ids) = &self.ids {
188            ids.write_to(&mut headers);
189        }
190        headers.insert(http::header::CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON));
191
192        let status = HttpStatus::from_u16(self.error.http_status()).unwrap_or(HttpStatus::OK);
193        let envelope: OcpiResponse<()> = self.error.to_response();
194        match serde_json::to_vec(&envelope) {
195            Ok(body) => (status, headers, body).into_response(),
196            Err(e) => internal_error(&format!("cannot serialise the error: {e}"), &headers),
197        }
198    }
199}
200
201impl IntoResponse for OcpiError {
202    fn into_response(self) -> Response {
203        OcpiErrorResponse::new(self).into_response()
204    }
205}
206
207/// The last resort, when even serialising the envelope failed.
208fn internal_error(message: &str, headers: &HeaderMap) -> Response {
209    let mut headers = headers.clone();
210    headers.remove(http::header::CONTENT_TYPE);
211    (HttpStatus::INTERNAL_SERVER_ERROR, headers, message.to_owned()).into_response()
212}
213
214/// Copies the request and correlation IDs onto a response, generating what is missing.
215#[must_use]
216pub fn echo_ids(request_headers: &HeaderMap) -> RequestIds {
217    RequestIds::from_headers_or_generate(request_headers)
218}
219
220/// Re-exported for handlers that build their own responses.
221pub use http::StatusCode as HttpStatusCode;
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::transport::StatusCode;
227    use crate::transport::headers::{X_CORRELATION_ID, X_REQUEST_ID};
228    use axum::body::to_bytes;
229
230    async fn body_of(response: Response) -> serde_json::Value {
231        let bytes = to_bytes(response.into_body(), 64 * 1024).await.unwrap();
232        serde_json::from_slice(&bytes).unwrap()
233    }
234
235    #[tokio::test]
236    async fn a_successful_reply_is_a_1000_envelope() {
237        let response = OcpiReply::ok(serde_json::json!({"id": "LOC1"})).into_response();
238        assert_eq!(response.status(), HttpStatus::OK);
239        assert_eq!(response.headers().get(http::header::CONTENT_TYPE).unwrap(), "application/json");
240        let body = body_of(response).await;
241        assert_eq!(body["status_code"], 1000);
242        assert_eq!(body["data"]["id"], "LOC1");
243    }
244
245    #[tokio::test]
246    async fn a_created_object_is_a_201_with_the_same_envelope() {
247        let response = OcpiReply::created(serde_json::json!({})).into_response();
248        assert_eq!(response.status(), HttpStatus::CREATED);
249        assert_eq!(body_of(response).await["status_code"], 1000);
250    }
251
252    #[tokio::test]
253    async fn an_unspecified_response_body_leaves_data_unset() {
254        let response = OcpiReply::<()>::no_content().into_response();
255        let body = body_of(response).await;
256        assert_eq!(body["status_code"], 1000);
257        assert!(body.get("data").is_none(), "the spec advises leaving `data` unset");
258    }
259
260    #[tokio::test]
261    async fn errors_that_reached_the_ocpi_layer_are_http_200() {
262        let response =
263            OcpiError::Decode { path: "/evses/0/status".to_owned(), message: "unknown value".to_owned() }
264                .into_response();
265        assert_eq!(response.status(), HttpStatus::OK, "an HTTP error code MUST NOT be returned");
266        let body = body_of(response).await;
267        assert_eq!(body["status_code"], StatusCode::INVALID_PARAMETERS.get());
268        assert!(body["status_message"].as_str().unwrap().contains("/evses/0/status"));
269    }
270
271    #[tokio::test]
272    async fn the_five_transport_level_failures_keep_their_http_status() {
273        for (error, expected) in [
274            (OcpiError::MalformedJson("nope".into()), 400),
275            (OcpiError::Unauthorized("no token".into()), 401),
276            (OcpiError::TokenAOutOfScope, 401),
277            (OcpiError::NotFound("/locations/1".into()), 404),
278            (OcpiError::MethodNotAllowed("already registered".into()), 405),
279        ] {
280            let response = error.into_response();
281            assert_eq!(response.status().as_u16(), expected);
282        }
283    }
284
285    #[tokio::test]
286    async fn the_request_and_correlation_ids_are_echoed() {
287        let ids = RequestIds::generate();
288        let response = OcpiReply::ok(1u8).with_ids(ids.clone()).into_response();
289        assert_eq!(response.headers().get(X_REQUEST_ID).unwrap(), ids.request_id.as_str());
290        assert_eq!(response.headers().get(X_CORRELATION_ID).unwrap(), ids.correlation_id.as_str());
291    }
292}