rustical_caldav/
error.rs

1use axum::{
2    body::Body,
3    response::{IntoResponse, Response},
4};
5use headers::{ContentType, HeaderMapExt};
6use http::StatusCode;
7use rustical_xml::{XmlSerialize, XmlSerializeRoot};
8use tracing::error;
9
10#[derive(Debug, thiserror::Error, XmlSerialize)]
11pub enum Precondition {
12    #[error("valid-calendar-data")]
13    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
14    ValidCalendarData,
15    #[error("calendar-timezone")]
16    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
17    CalendarTimezone(&'static str),
18}
19
20impl IntoResponse for Precondition {
21    fn into_response(self) -> axum::response::Response {
22        let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
23        let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
24
25        let error = rustical_dav::xml::ErrorElement(&self);
26        if let Err(err) = error.serialize_root(&mut writer) {
27            return rustical_dav::Error::from(err).into_response();
28        }
29        let mut res = Response::builder().status(StatusCode::FORBIDDEN);
30        res.headers_mut().unwrap().typed_insert(ContentType::xml());
31        res.body(Body::from(output)).unwrap()
32    }
33}
34
35#[derive(Debug, thiserror::Error)]
36pub enum Error {
37    #[error("Unauthorized")]
38    Unauthorized,
39
40    #[error("Not Found")]
41    NotFound,
42
43    #[error("Not implemented")]
44    NotImplemented,
45
46    #[error(transparent)]
47    StoreError(#[from] rustical_store::Error),
48
49    #[error(transparent)]
50    ChronoParseError(#[from] chrono::ParseError),
51
52    #[error(transparent)]
53    DavError(#[from] rustical_dav::Error),
54
55    #[error(transparent)]
56    XmlDecodeError(#[from] rustical_xml::XmlError),
57
58    #[error(transparent)]
59    PreconditionFailed(Precondition),
60}
61
62impl Error {
63    #[must_use]
64    pub fn status_code(&self) -> StatusCode {
65        match self {
66            Self::StoreError(err) => match err {
67                rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
68                rustical_store::Error::AlreadyExists => StatusCode::CONFLICT,
69                rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
70                _ => StatusCode::INTERNAL_SERVER_ERROR,
71            },
72            Self::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
73                .expect("Just converting between versions"),
74            Self::Unauthorized => StatusCode::UNAUTHORIZED,
75            Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
76            Self::ChronoParseError(_) | Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
77            Self::NotFound => StatusCode::NOT_FOUND,
78            // The correct status code for a failed precondition is not PreconditionFailed but
79            // Forbidden (or Conflict):
80            // https://datatracker.ietf.org/doc/html/rfc4791#section-1.3
81            Self::PreconditionFailed(_err) => StatusCode::FORBIDDEN,
82        }
83    }
84}
85
86impl IntoResponse for Error {
87    fn into_response(self) -> axum::response::Response {
88        if let Self::PreconditionFailed(precondition) = self {
89            return precondition.into_response();
90        }
91        if matches!(self.status_code(), StatusCode::INTERNAL_SERVER_ERROR) {
92            error!("{self}");
93        }
94        (self.status_code(), self.to_string()).into_response()
95    }
96}