Skip to main content

durable_streams_server/protocol/
problem.rs

1use axum::{
2    body::Body,
3    extract::OriginalUri,
4    http::{
5        HeaderMap, HeaderValue, StatusCode,
6        header::{CONTENT_TYPE, IntoHeaderName},
7    },
8    response::{IntoResponse, Response},
9};
10use serde::{Deserialize, Serialize};
11
12/// RFC 9457 problem details payload used for server error responses.
13///
14/// The field names intentionally match the wire format so the same type can be
15/// reused by future client-side parsing without another translation layer.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct ProblemDetails {
18    /// URI identifying the problem type.
19    #[serde(rename = "type")]
20    pub problem_type: String,
21    /// Short, human-readable summary of the problem type.
22    pub title: String,
23    /// HTTP status code. Must match the response status line.
24    pub status: u16,
25    /// Machine-readable error code.
26    pub code: String,
27    /// Per-instance explanation of the error.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub detail: Option<String>,
30    /// Request path or URI for the failing resource.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub instance: Option<String>,
33}
34
35impl ProblemDetails {
36    /// Create a new problem details payload with the required RFC 9457 fields.
37    #[must_use]
38    pub fn new(
39        problem_type: &'static str,
40        title: &'static str,
41        status: StatusCode,
42        code: &'static str,
43    ) -> Self {
44        Self {
45            problem_type: problem_type.to_string(),
46            title: title.to_string(),
47            status: status.as_u16(),
48            code: code.to_string(),
49            detail: None,
50            instance: None,
51        }
52    }
53
54    /// Attach a problem-specific detail string.
55    #[must_use]
56    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
57        self.detail = Some(detail.into());
58        self
59    }
60
61    /// Attach the request instance path/URI.
62    #[must_use]
63    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
64        self.instance = Some(instance.into());
65        self
66    }
67}
68
69/// Telemetry metadata copied from the final problem response.
70#[derive(Debug, Clone)]
71pub struct ProblemTelemetry {
72    pub problem_type: String,
73    pub code: String,
74    pub title: String,
75    pub detail: Option<String>,
76    pub error_class: Option<String>,
77    pub storage_backend: Option<String>,
78    pub storage_operation: Option<String>,
79    pub internal_detail: Option<String>,
80    pub retry_after_secs: Option<u32>,
81}
82
83impl From<&ProblemDetails> for ProblemTelemetry {
84    fn from(problem: &ProblemDetails) -> Self {
85        Self {
86            problem_type: problem.problem_type.clone(),
87            code: problem.code.clone(),
88            title: problem.title.clone(),
89            detail: problem.detail.clone(),
90            error_class: None,
91            storage_backend: None,
92            storage_operation: None,
93            internal_detail: None,
94            retry_after_secs: None,
95        }
96    }
97}
98
99/// Builder for structured error responses with protocol-specific headers.
100#[derive(Debug, Clone)]
101pub struct ProblemResponse {
102    problem: ProblemDetails,
103    headers: HeaderMap,
104    telemetry: Option<ProblemTelemetry>,
105}
106
107/// Response result alias for handlers that emit structured problem details.
108pub type Result<T> = std::result::Result<T, ProblemResponse>;
109
110impl ProblemResponse {
111    /// Create a problem response from an RFC 9457 payload.
112    #[must_use]
113    pub fn new(problem: ProblemDetails) -> Self {
114        Self {
115            problem,
116            headers: HeaderMap::new(),
117            telemetry: None,
118        }
119    }
120
121    /// Attach a problem detail.
122    #[must_use]
123    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
124        self.problem.detail = Some(detail.into());
125        self
126    }
127
128    /// Attach the request instance path/URI.
129    #[must_use]
130    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
131        self.problem.instance = Some(instance.into());
132        self
133    }
134
135    /// Attach an additional response header.
136    #[must_use]
137    pub fn with_header<K>(mut self, key: K, value: HeaderValue) -> Self
138    where
139        K: IntoHeaderName,
140    {
141        self.headers.insert(key, value);
142        self
143    }
144
145    /// Attach explicit telemetry metadata that differs from the public problem payload.
146    #[must_use]
147    pub fn with_telemetry(mut self, telemetry: ProblemTelemetry) -> Self {
148        self.telemetry = Some(telemetry);
149        self
150    }
151}
152
153/// Convert the actual request target into a relative `instance` reference.
154///
155/// This intentionally excludes scheme/authority information so error payloads
156/// do not expose proxy or load-balancer host configuration to clients.
157#[must_use]
158pub fn request_instance(OriginalUri(uri): &OriginalUri) -> String {
159    uri.path_and_query()
160        .map(|value| value.as_str().to_string())
161        .unwrap_or_else(|| uri.path().to_string())
162}
163
164impl IntoResponse for ProblemResponse {
165    fn into_response(self) -> Response {
166        let status =
167            StatusCode::from_u16(self.problem.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
168        let telemetry = self
169            .telemetry
170            .unwrap_or_else(|| ProblemTelemetry::from(&self.problem));
171        let body = match serde_json::to_vec(&self.problem) {
172            Ok(body) => body,
173            Err(_) => br#"{"type":"/errors/internal","title":"Internal Server Error","status":500,"code":"INTERNAL_ERROR"}"#.to_vec(),
174        };
175
176        let mut response = Response::new(Body::from(body));
177        *response.status_mut() = status;
178
179        let headers = response.headers_mut();
180        headers.insert(
181            CONTENT_TYPE,
182            HeaderValue::from_static("application/problem+json"),
183        );
184        headers.extend(self.headers);
185
186        response.extensions_mut().insert(telemetry);
187        response
188    }
189}