Skip to main content

mdcast_api/wire/
error.rs

1//! The generic error body every 4xx/5xx other than `409 Conflict` carries.
2//! `409` has its own shape ([`crate::MissingBlobsError`]) because a client
3//! acts on it rather than reporting it.
4
5use serde::{Deserialize, Serialize};
6
7/// A failed request: something for a human (`message`) and something for the
8/// client to branch on (`code`).
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct ErrorResponse {
11    pub code: ErrorCode,
12    pub message: String,
13}
14
15impl ErrorResponse {
16    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
17        Self {
18            code,
19            message: message.into(),
20        }
21    }
22}
23
24/// Machine-readable failure classification.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum ErrorCode {
28    /// The body did not parse, or a field is out of range.
29    BadRequest,
30    /// No bearer token, a malformed `Authorization` header, or the wrong
31    /// token. Distinct from [`ErrorCode::BadRequest`] because the client's
32    /// remedy is to fix its credentials, not its payload.
33    Unauthorized,
34    /// The request body is larger than the server accepts. Distinct from
35    /// [`ErrorCode::BadRequest`] because retrying the identical request is
36    /// hopeless — the client has to split or shrink the upload.
37    PayloadTooLarge,
38    /// The server was built without the feature this target/format needs
39    /// (e.g. `html` without `typst-html`), or lacks the `pandoc` binary.
40    UnsupportedTarget,
41    /// The pipeline ran and failed — bad markdown, a template that does not
42    /// compile, an engine error.
43    RenderFailed,
44    /// Anything the server cannot attribute to the request.
45    Internal,
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn error_response_round_trips_through_json() {
54        let err = ErrorResponse::new(ErrorCode::RenderFailed, "typst: unclosed delimiter");
55
56        let json = serde_json::to_string(&err).unwrap();
57        assert_eq!(
58            json,
59            r#"{"code":"render_failed","message":"typst: unclosed delimiter"}"#
60        );
61        assert_eq!(serde_json::from_str::<ErrorResponse>(&json).unwrap(), err);
62    }
63
64    #[test]
65    fn error_codes_serialize_snake_case() {
66        for (code, expected) in [
67            (ErrorCode::BadRequest, "\"bad_request\""),
68            (ErrorCode::Unauthorized, "\"unauthorized\""),
69            (ErrorCode::PayloadTooLarge, "\"payload_too_large\""),
70            (ErrorCode::UnsupportedTarget, "\"unsupported_target\""),
71            (ErrorCode::RenderFailed, "\"render_failed\""),
72            (ErrorCode::Internal, "\"internal\""),
73        ] {
74            let json = serde_json::to_string(&code).unwrap();
75            assert_eq!(json, expected);
76            assert_eq!(serde_json::from_str::<ErrorCode>(&json).unwrap(), code);
77        }
78    }
79}