sfr_server/
error.rs

1//! An Error to respond a HTTP status.
2
3use sfr_types as st;
4
5use crate::SlashCommandResponse;
6use axum::http::StatusCode;
7use axum::response::{IntoResponse, Response};
8use axum::BoxError;
9use axum::Json;
10use serde_json::json;
11
12/// The type to respond a HTTP status.
13#[derive(Debug)]
14pub enum ResponseError {
15    /// The Slack request has unexpected headers.
16    InvalidHeader(String),
17
18    /// Failed to read the request body.
19    ReadBody,
20
21    /// Failed to deserialize the request body.
22    DeserializeBody(BoxError),
23
24    /// The Slack Verification Token is not matched.
25    InvalidToken,
26
27    /// The Method is not `POST`.
28    ///
29    /// > A payload is sent via an HTTP POST request to your app.
30    ///
31    /// <https://api.slack.com/interactivity/slash-commands#getting_started>
32    MethodNotAllowed,
33
34    /// Occurred an error in the user source.
35    InternalServerError(Box<dyn std::error::Error + 'static + Send + Sync>),
36
37    /// Occurred an unexpected state.
38    Unknown,
39
40    /// The custom error from the user source.
41    Custom(StatusCode, String),
42
43    /// The custom response for fast-return.
44    CustomResponse(SlashCommandResponse),
45}
46
47impl ResponseError {
48    /// Creates [`ResponseError::InternalServerError`].
49    pub fn internal_server_error(inner: impl std::error::Error + 'static + Send + Sync) -> Self {
50        Self::InternalServerError(Box::new(inner))
51    }
52
53    /// Creates a custom response.
54    pub fn custom_response<T>(resp: T) -> Self
55    where
56        T: Into<SlashCommandResponse>,
57    {
58        Self::CustomResponse(resp.into())
59    }
60
61    /// Creates a custom error.
62    pub fn custom<T1, T2>(status: T1, message: T2) -> Result<Self, st::Error>
63    where
64        T1: TryInto<StatusCode>,
65        <T1 as TryInto<StatusCode>>::Error: std::error::Error + 'static + Send + Sync,
66        T2: Into<String>,
67    {
68        let status = status
69            .try_into()
70            .map_err(|e| st::ServerError::InvalidStatusCode(Box::new(e)))?;
71        Ok(Self::Custom(status, message.into()))
72    }
73
74    /// Maps the tuple of the HTTP [`StatusCode`] and the error message from [`ResponseError`].
75    fn map(&self) -> (StatusCode, &str) {
76        match self {
77            Self::InvalidHeader(_) => (StatusCode::BAD_REQUEST, "Invalid header"),
78            Self::ReadBody => (StatusCode::BAD_REQUEST, "Failed to read body"),
79            Self::DeserializeBody(_) => (StatusCode::BAD_REQUEST, "Failed to deserialize"),
80            Self::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid body"),
81
82            Self::MethodNotAllowed => (StatusCode::METHOD_NOT_ALLOWED, "method not allowed"),
83
84            Self::InternalServerError(_) | Self::Unknown => {
85                (StatusCode::INTERNAL_SERVER_ERROR, "Unknown error")
86            }
87
88            Self::Custom(code, message) => (*code, message.as_str()),
89
90            Self::CustomResponse(_) => unreachable!(),
91        }
92    }
93}
94
95impl IntoResponse for ResponseError {
96    fn into_response(self) -> Response {
97        if let Self::CustomResponse(resp) = self {
98            return resp.into_response();
99        }
100
101        tracing::info!("response error: {self:?}");
102
103        let (status, error_message) = self.map();
104        let body = Json(json!({
105            "error": error_message,
106        }));
107        (status, body).into_response()
108    }
109}
110
111impl std::fmt::Display for ResponseError {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
113        let (status, error_message) = self.map();
114        write!(f, "{status}: {error_message}")
115    }
116}
117
118impl std::error::Error for ResponseError {}