Skip to main content

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 error: {0}")]
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 error.serialize_root(&mut writer).is_err() {
27            // Should never throw since the std::io::Write implementation for Vec<u8> never throws
28            return (
29                StatusCode::INTERNAL_SERVER_ERROR,
30                "IO error when serialising output",
31            )
32                .into_response();
33        }
34        let mut res = Response::builder().status(StatusCode::FORBIDDEN);
35        res.headers_mut().unwrap().typed_insert(ContentType::xml());
36        res.body(Body::from(output)).unwrap()
37    }
38}
39
40#[derive(Debug, thiserror::Error)]
41pub enum Error {
42    #[error("Unauthorized")]
43    Unauthorized,
44
45    #[error("Not Found")]
46    NotFound,
47
48    #[error("Not implemented")]
49    NotImplemented,
50
51    #[error(transparent)]
52    StoreError(#[from] rustical_store::Error),
53
54    #[error(transparent)]
55    ChronoParseError(#[from] chrono::ParseError),
56
57    #[error(transparent)]
58    DavError(#[from] rustical_dav::Error),
59
60    #[error(transparent)]
61    XmlDecodeError(#[from] rustical_xml::XmlError),
62
63    #[error(transparent)]
64    PreconditionFailed(Precondition),
65}
66
67impl Error {
68    #[must_use]
69    pub fn status_code(&self) -> StatusCode {
70        match self {
71            Self::StoreError(err) => match err {
72                rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
73                rustical_store::Error::AlreadyExists => StatusCode::CONFLICT,
74                rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
75                _ => StatusCode::INTERNAL_SERVER_ERROR,
76            },
77            Self::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
78                .expect("Just converting between versions"),
79            Self::Unauthorized => StatusCode::UNAUTHORIZED,
80            Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
81            Self::ChronoParseError(_) | Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
82            Self::NotFound => StatusCode::NOT_FOUND,
83            // The correct status code for a failed precondition is not PreconditionFailed but
84            // Forbidden (or Conflict):
85            // https://datatracker.ietf.org/doc/html/rfc4791#section-1.3
86            Self::PreconditionFailed(_err) => StatusCode::FORBIDDEN,
87        }
88    }
89}
90
91impl IntoResponse for Error {
92    fn into_response(self) -> axum::response::Response {
93        if let Self::PreconditionFailed(precondition) = self {
94            return precondition.into_response();
95        }
96        if matches!(self.status_code(), StatusCode::INTERNAL_SERVER_ERROR) {
97            error!("{self}");
98        }
99        (self.status_code(), self.to_string()).into_response()
100    }
101}