Skip to main content

tako_rs_plugins/middleware/
problem_json.rs

1//! `application/problem+json` error normalizer middleware.
2//!
3//! Wraps the response so 4xx and 5xx replies that lack a JSON body are
4//! rewritten into RFC 7807 / RFC 9457 problem documents. Handlers that
5//! already produced a structured JSON error stay authoritative — the
6//! `Content-Type` of the original response is the trigger.
7//!
8//! Sister to [`Router::use_problem_json`](tako_rs_core::router::Router::use_problem_json)
9//! / [`tako::problem::default_problem_responder`](tako_rs_core::problem::default_problem_responder).
10//! The router hook fires only when the response originated from the framework
11//! itself (e.g. 404, 405, default error handler), whereas this middleware
12//! converts any 4xx/5xx that bubbles up from handler code.
13
14use std::future::Future;
15use std::pin::Pin;
16
17use tako_rs_core::middleware::IntoMiddleware;
18use tako_rs_core::middleware::Next;
19use tako_rs_core::problem::default_problem_responder;
20use tako_rs_core::types::Request;
21use tako_rs_core::types::Response;
22
23/// Middleware that rewrites non-JSON 4xx/5xx responses into `problem+json`.
24pub struct ProblemJson {
25  /// Convert 4xx responses (default true).
26  client_errors: bool,
27  /// Convert 5xx responses (default true).
28  server_errors: bool,
29}
30
31impl Default for ProblemJson {
32  fn default() -> Self {
33    Self::new()
34  }
35}
36
37impl ProblemJson {
38  /// Creates the middleware with both 4xx and 5xx conversion enabled.
39  pub fn new() -> Self {
40    Self {
41      client_errors: true,
42      server_errors: true,
43    }
44  }
45
46  /// Toggles 4xx → problem+json conversion.
47  pub fn client_errors(mut self, on: bool) -> Self {
48    self.client_errors = on;
49    self
50  }
51
52  /// Toggles 5xx → problem+json conversion.
53  pub fn server_errors(mut self, on: bool) -> Self {
54    self.server_errors = on;
55    self
56  }
57}
58
59impl IntoMiddleware for ProblemJson {
60  fn into_middleware(
61    self,
62  ) -> impl Fn(Request, Next) -> Pin<Box<dyn Future<Output = Response> + Send + 'static>>
63  + Clone
64  + Send
65  + Sync
66  + 'static {
67    let client = self.client_errors;
68    let server = self.server_errors;
69
70    move |req: Request, next: Next| {
71      Box::pin(async move {
72        let resp = next.run(req).await;
73        let status = resp.status();
74        let should_convert =
75          (client && status.is_client_error()) || (server && status.is_server_error());
76        if !should_convert {
77          return resp;
78        }
79        default_problem_responder(resp)
80      })
81    }
82  }
83}