1use std::{
4 future::Future,
5 pin::Pin,
6 task::{Context, Poll},
7};
8
9use axum::{Json, body::Body, body::to_bytes, extract::Request, response::IntoResponse};
10use http::StatusCode;
11use serde::{Deserialize, Serialize};
12use tower::{Layer, Service};
13
14use crate::context::RequestContext;
15
16#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
29#[error("{message}")]
30pub struct HttpError {
31 status: StatusCode,
32 code: &'static str,
33 message: String,
34}
35
36impl HttpError {
37 pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
43 Self {
44 status,
45 code,
46 message: message.into(),
47 }
48 }
49
50 pub fn bad_request(message: impl Into<String>) -> Self {
52 Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
53 }
54
55 pub fn unauthorized(message: impl Into<String>) -> Self {
57 Self::new(StatusCode::UNAUTHORIZED, "unauthorized", message)
58 }
59
60 pub fn forbidden(message: impl Into<String>) -> Self {
62 Self::new(StatusCode::FORBIDDEN, "forbidden", message)
63 }
64
65 pub fn not_found(message: impl Into<String>) -> Self {
67 Self::new(StatusCode::NOT_FOUND, "not_found", message)
68 }
69
70 pub fn conflict(message: impl Into<String>) -> Self {
72 Self::new(StatusCode::CONFLICT, "conflict", message)
73 }
74
75 pub fn too_many_requests(message: impl Into<String>) -> Self {
77 Self::new(StatusCode::TOO_MANY_REQUESTS, "too_many_requests", message)
78 }
79
80 pub fn unprocessable_entity(message: impl Into<String>) -> Self {
82 Self::new(
83 StatusCode::UNPROCESSABLE_ENTITY,
84 "unprocessable_entity",
85 message,
86 )
87 }
88
89 pub fn internal_server_error() -> Self {
94 Self::new(
95 StatusCode::INTERNAL_SERVER_ERROR,
96 "internal_server_error",
97 "internal server error",
98 )
99 }
100
101 pub fn status(&self) -> StatusCode {
103 self.status
104 }
105
106 pub fn code(&self) -> &'static str {
108 self.code
109 }
110
111 pub fn message(&self) -> &str {
113 &self.message
114 }
115}
116
117impl IntoResponse for HttpError {
118 fn into_response(self) -> axum::response::Response {
119 let status = self.status;
120 let code = self.code;
121 let message = self.message;
122
123 if status.is_server_error() {
124 tracing::error!(
125 http.status = status.as_u16(),
126 error.code = code,
127 error.message = %message,
128 "http error response"
129 );
130 } else {
131 tracing::warn!(
132 http.status = status.as_u16(),
133 error.code = code,
134 error.message = %message,
135 "http error response"
136 );
137 }
138
139 let body = Json(ErrorBody {
140 error: ErrorDetails { code, message },
141 });
142 (status, body).into_response()
143 }
144}
145
146pub async fn not_found_fallback() -> HttpError {
152 HttpError::not_found("route not found")
153}
154
155#[derive(Debug, Serialize)]
156struct ErrorBody {
157 error: ErrorDetails,
158}
159
160#[derive(Debug, Serialize)]
161struct ErrorDetails {
162 code: &'static str,
163 message: String,
164}
165
166#[derive(Clone, Copy, Debug, Default)]
200pub struct ErrorEnvelopeLayer;
201
202impl ErrorEnvelopeLayer {
203 pub fn new() -> Self {
205 Self
206 }
207}
208
209impl<S> Layer<S> for ErrorEnvelopeLayer {
210 type Service = ErrorEnvelopeService<S>;
211
212 fn layer(&self, inner: S) -> Self::Service {
213 ErrorEnvelopeService { inner }
214 }
215}
216
217#[derive(Clone, Debug)]
219pub struct ErrorEnvelopeService<S> {
220 inner: S,
221}
222
223impl<S> Service<Request> for ErrorEnvelopeService<S>
224where
225 S: Service<Request, Response = axum::response::Response> + Send + 'static,
226 S::Future: Send + 'static,
227 S::Error: Send + 'static,
228{
229 type Response = axum::response::Response;
230 type Error = S::Error;
231 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
232
233 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
234 self.inner.poll_ready(cx)
235 }
236
237 fn call(&mut self, request: Request) -> Self::Future {
238 let uri = request.uri().clone();
242 let request_context = request.extensions().get::<RequestContext>().cloned();
243 let future = self.inner.call(request);
244
245 Box::pin(async move {
246 let response = future.await?;
247 if !response.status().is_client_error() && !response.status().is_server_error() {
248 return Ok(response);
249 }
250 let request_id = request_context
251 .as_ref()
252 .map(|context| context.request_id().to_owned());
253 let path = uri.path().to_owned();
254 Ok(envelope_response(response, request_id, path).await)
255 })
256 }
257}
258
259async fn envelope_response(
260 response: axum::response::Response,
261 request_id: Option<String>,
262 path: String,
263) -> axum::response::Response {
264 let (mut parts, body) = response.into_parts();
265 let status = parts.status;
266 let extracted = read_legacy_error_body(body).await;
267 let mut code = extracted
268 .as_ref()
269 .map(|body| body.error.code.clone())
270 .unwrap_or_else(|| default_code(status).to_owned());
271 let mut message = extracted
272 .as_ref()
273 .map(|body| body.error.message.clone())
274 .unwrap_or_else(|| status.canonical_reason().unwrap_or("error").to_owned());
275 let mut details = extracted
276 .map(|body| {
277 if body.error.details.is_empty() {
278 serde_json::Value::Null
279 } else {
280 serde_json::Value::Object(body.error.details)
281 }
282 })
283 .unwrap_or(serde_json::Value::Null);
284 if status.is_server_error() {
285 tracing::error!(
286 http.status = status.as_u16(),
287 error.code = %code,
288 request.id = request_id.as_deref().unwrap_or(""),
289 http.path = %path,
290 "http error envelope"
291 );
292 message = "internal server error".to_owned();
295 details = serde_json::Value::Null;
296 code = default_code(status).to_owned();
297 }
298
299 let envelope = ProductionErrorBody {
300 error: ProductionErrorDetails {
301 status_code: status.as_u16(),
302 code,
303 message,
304 details,
305 timestamp: timestamp_now(),
306 path,
307 request_id: request_id.unwrap_or_default(),
308 },
309 };
310 let body = serde_json::to_vec(&envelope).expect("error envelope should serialize");
311 parts.headers.remove(http::header::CONTENT_LENGTH);
315 parts.headers.remove(http::header::CONTENT_ENCODING);
316 parts.headers.remove(http::header::CONTENT_RANGE);
317 parts.headers.insert(
318 http::header::CONTENT_TYPE,
319 http::HeaderValue::from_static("application/json"),
320 );
321 axum::response::Response::from_parts(parts, Body::from(body))
322}
323
324const MAX_ERROR_ENVELOPE_BODY_BYTES: usize = 64 * 1024;
325
326async fn read_legacy_error_body(body: Body) -> Option<LegacyErrorBody> {
327 let bytes = to_bytes(body, MAX_ERROR_ENVELOPE_BODY_BYTES).await.ok()?;
328 serde_json::from_slice::<LegacyErrorBody>(&bytes).ok()
329}
330
331pub(crate) fn timestamp_now() -> String {
333 time::OffsetDateTime::now_utc()
334 .format(&time::format_description::well_known::Rfc3339)
335 .expect("UTC timestamp should format as RFC3339")
336}
337
338fn default_code(status: StatusCode) -> &'static str {
339 match status {
340 StatusCode::BAD_REQUEST => "bad_request",
341 StatusCode::UNAUTHORIZED => "unauthorized",
342 StatusCode::FORBIDDEN => "forbidden",
343 StatusCode::NOT_FOUND => "not_found",
344 StatusCode::CONFLICT => "conflict",
345 StatusCode::UNPROCESSABLE_ENTITY => "unprocessable_entity",
346 StatusCode::TOO_MANY_REQUESTS => "too_many_requests",
347 status if status.is_server_error() => "internal_server_error",
348 _ => "http_error",
349 }
350}
351
352#[derive(Debug, Deserialize)]
353struct LegacyErrorBody {
354 error: LegacyErrorDetails,
355}
356
357#[derive(Debug, Deserialize)]
358struct LegacyErrorDetails {
359 code: String,
360 message: String,
361 #[serde(flatten)]
362 details: serde_json::Map<String, serde_json::Value>,
363}
364
365#[derive(Debug, Serialize)]
366struct ProductionErrorBody {
367 error: ProductionErrorDetails,
368}
369
370#[derive(Debug, Serialize)]
371#[serde(rename_all = "camelCase")]
372struct ProductionErrorDetails {
373 status_code: u16,
374 code: String,
375 message: String,
376 details: serde_json::Value,
377 timestamp: String,
378 path: String,
379 request_id: String,
380}
381
382#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
384#[error("route path `{path}` contains a parameter segment without a name after ':'")]
385pub struct RoutePathError {
386 path: String,
387}
388
389impl RoutePathError {
390 pub fn empty_parameter(path: impl Into<String>) -> Self {
392 Self { path: path.into() }
393 }
394
395 pub fn path(&self) -> &str {
397 &self.path
398 }
399}