Skip to main content

ocpp_service/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! HTTP API error type.
5
6use axum::Json;
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9
10#[derive(Debug)]
11pub enum AppError {
12    BadRequest(String),
13    NotFound(String),
14    Conflict(String),
15    Unauthorized,
16    /// The station is not connected to this node (it may be connected to `node`).
17    NotConnected {
18        node: Option<String>,
19    },
20    Unavailable(String),
21    GatewayTimeout(String),
22    /// The station answered with a CALLERROR.
23    Upstream(ocpp_core::RpcError),
24    Internal(String),
25}
26
27impl std::fmt::Display for AppError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{self:?}")
30    }
31}
32
33impl From<sqlx::Error> for AppError {
34    fn from(e: sqlx::Error) -> Self {
35        if let sqlx::Error::Database(db) = &e
36            && db.is_unique_violation()
37        {
38            return AppError::Conflict(db.message().to_owned());
39        }
40        if let sqlx::Error::Database(db) = &e
41            && db.is_foreign_key_violation()
42        {
43            return AppError::Conflict(format!("still referenced: {}", db.message()));
44        }
45        if let sqlx::Error::Database(db) = &e
46            && db.is_check_violation()
47        {
48            return AppError::BadRequest(db.message().to_owned());
49        }
50        AppError::Internal(e.to_string())
51    }
52}
53
54impl IntoResponse for AppError {
55    fn into_response(self) -> Response {
56        let (status, body) = match self {
57            AppError::BadRequest(m) => (
58                StatusCode::BAD_REQUEST,
59                serde_json::json!({ "error": "bad_request", "message": m }),
60            ),
61            AppError::NotFound(m) => (
62                StatusCode::NOT_FOUND,
63                serde_json::json!({ "error": "not_found", "message": m }),
64            ),
65            AppError::Conflict(m) => (
66                StatusCode::CONFLICT,
67                serde_json::json!({ "error": "conflict", "message": m }),
68            ),
69            AppError::Unauthorized => (
70                StatusCode::UNAUTHORIZED,
71                serde_json::json!({ "error": "unauthorized" }),
72            ),
73            AppError::NotConnected { node } => (
74                StatusCode::CONFLICT,
75                serde_json::json!({ "error": "station_not_connected", "message": "station is not connected to this node", "connected_node": node }),
76            ),
77            AppError::Unavailable(m) => (
78                StatusCode::SERVICE_UNAVAILABLE,
79                serde_json::json!({ "error": "unavailable", "message": m }),
80            ),
81            AppError::GatewayTimeout(m) => (
82                StatusCode::GATEWAY_TIMEOUT,
83                serde_json::json!({ "error": "timeout", "message": m }),
84            ),
85            AppError::Upstream(e) => (
86                StatusCode::BAD_GATEWAY,
87                serde_json::json!({ "error": "call_error", "error_code": e.code.to_string(), "description": e.description, "details": e.details }),
88            ),
89            AppError::Internal(m) => {
90                tracing::error!(error = %m, "internal error");
91                (
92                    StatusCode::INTERNAL_SERVER_ERROR,
93                    serde_json::json!({ "error": "internal" }),
94                )
95            }
96        };
97        (status, Json(body)).into_response()
98    }
99}