Skip to main content

rws_http/error/
mod.rs

1#[cfg(test)]
2mod tests;
3
4pub mod problem;
5pub use problem::problem;
6
7use crate::header::Header;
8use crate::request::Request;
9use crate::response::{Response, StatusCodeReasonPhrase, STATUS_CODE_REASON_PHRASE};
10
11/// Implemented by any type that can be turned into an HTTP [`Response`].
12///
13/// Implement this on your application error enum so handlers can return
14/// `Result<Response, MyError>` and the framework maps the error to the
15/// correct HTTP status automatically.
16///
17/// [`Response`] itself implements `IntoResponse` as the identity conversion.
18pub trait IntoResponse {
19    fn into_response(self) -> Response;
20}
21
22impl IntoResponse for Response {
23    fn into_response(self) -> Response {
24        self
25    }
26}
27
28/// `Ok` and `Err` each become their own response, so a handler can return
29/// `Result<Json<Post>, AppError>` (18.28.0).
30impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
31    fn into_response(self) -> Response {
32        match self {
33            Ok(value) => value.into_response(),
34            Err(error) => error.into_response(),
35        }
36    }
37}
38
39/// `200 OK`, `text/plain` (18.28.0).
40impl IntoResponse for String {
41    fn into_response(self) -> Response {
42        Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, &self)
43    }
44}
45
46/// `200 OK`, `text/plain` (18.28.0).
47impl IntoResponse for &'static str {
48    fn into_response(self) -> Response {
49        Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, self)
50    }
51}
52
53/// The inner response with its status replaced:
54/// `(STATUS_CODE_REASON_PHRASE.n201_created, Json(post))` (18.28.0).
55impl<T: IntoResponse> IntoResponse for (&'static StatusCodeReasonPhrase, T) {
56    fn into_response(self) -> Response {
57        let (status, inner) = self;
58        let mut response = inner.into_response();
59        response.status_code = *status.status_code;
60        response.reason_phrase = status.reason_phrase.to_string();
61        response
62    }
63}
64
65/// A built-in typed error that maps common failure cases to standard HTTP
66/// status codes. Use it directly or as a model for your own error type.
67///
68/// # Example
69///
70/// ```rust,no_run
71/// # extern crate rws_http as rust_web_server;
72/// use rust_web_server::error::{AppError, IntoResponse};
73/// use rust_web_server::response::Response;
74///
75/// fn find_user(id: u64) -> Result<Response, AppError> {
76///     if id == 0 {
77///         return Err(AppError::NotFound("user not found".to_string()));
78///     }
79///     Err(AppError::Internal("db error".to_string()))
80/// }
81///
82/// // In your controller:
83/// // let response = find_user(id).unwrap_or_else(|e| e.into_response());
84/// ```
85#[derive(Debug, PartialEq, Eq)]
86pub enum AppError {
87    /// 400 Bad Request — malformed input.
88    BadRequest(String),
89    /// 401 Unauthorized — authentication is required.
90    Unauthorized,
91    /// 403 Forbidden — authenticated but not permitted.
92    Forbidden,
93    /// 404 Not Found — the requested resource does not exist.
94    NotFound(String),
95    /// 409 Conflict — the request conflicts with current state.
96    Conflict(String),
97    /// 422 Unprocessable Entity — input is syntactically valid but semantically wrong.
98    UnprocessableEntity(String),
99    /// 429 Too Many Requests — client has exceeded the rate limit.
100    TooManyRequests,
101    /// 413 Payload Too Large — the request body exceeds a configured limit.
102    PayloadTooLarge(String),
103    /// 500 Internal Server Error — unexpected server-side failure.
104    Internal(String),
105}
106
107impl IntoResponse for AppError {
108    fn into_response(self) -> Response {
109        let (status, body_str) = match &self {
110            AppError::BadRequest(msg)           => (STATUS_CODE_REASON_PHRASE.n400_bad_request, msg.as_str()),
111            AppError::Unauthorized              => (STATUS_CODE_REASON_PHRASE.n401_unauthorized, "Unauthorized"),
112            AppError::Forbidden                 => (STATUS_CODE_REASON_PHRASE.n403_forbidden, "Forbidden"),
113            AppError::NotFound(msg)             => (STATUS_CODE_REASON_PHRASE.n404_not_found, msg.as_str()),
114            AppError::Conflict(msg)             => (STATUS_CODE_REASON_PHRASE.n409_conflict, msg.as_str()),
115            AppError::UnprocessableEntity(msg)  => (STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity, msg.as_str()),
116            AppError::TooManyRequests           => (STATUS_CODE_REASON_PHRASE.n429_too_many_requests, "Too Many Requests"),
117            AppError::PayloadTooLarge(msg)       => (STATUS_CODE_REASON_PHRASE.n413_payload_too_large, msg.as_str()),
118            AppError::Internal(msg)             => (STATUS_CODE_REASON_PHRASE.n500_internal_server_error, msg.as_str()),
119        };
120
121        // Problem details (18.17.0); `error_format = "text"` keeps the bare
122        // message as the body.
123        let detail = match &self {
124            AppError::Unauthorized | AppError::Forbidden | AppError::TooManyRequests => "",
125            _ => body_str,
126        };
127        let dummy = Request {
128            method: "GET".to_string(),
129            request_uri: "/".to_string(),
130            http_version: "HTTP/1.1".to_string(),
131            headers: vec![],
132            body: Default::default(),
133        };
134        problem::with_headers(status, detail, body_str, Some(Header::get_header_list(&dummy)))
135    }
136}