1use crate::error::Result;
44use crate::middleware::{Middleware, Next};
45use crate::request::Request;
46use crate::response::Response;
47use async_trait::async_trait;
48
49#[derive(Clone, Debug, Default)]
51pub struct ProblemDetails {
52 type_base: Option<String>,
53 include_instance: bool,
54}
55
56impl ProblemDetails {
57 pub fn new() -> Self {
58 Self {
59 type_base: None,
60 include_instance: true,
61 }
62 }
63
64 pub fn type_base(mut self, base: &str) -> Self {
66 self.type_base = Some(base.to_string());
67 self
68 }
69
70 pub fn without_instance(mut self) -> Self {
72 self.include_instance = false;
73 self
74 }
75
76 fn convert(&self, res: Response, path: &str) -> Response {
77 if !res.status.is_client_error() && !res.status.is_server_error() {
78 return res;
79 }
80 let is_json = res
81 .headers
82 .get(http::header::CONTENT_TYPE)
83 .and_then(|v| v.to_str().ok())
84 .is_some_and(|v| v.starts_with("application/json"));
85 if !is_json || res.is_streaming() {
86 return res;
87 }
88 let Ok(serde_json::Value::Object(old)) = serde_json::from_slice(&res.body) else {
89 return res;
90 };
91 let known = old
92 .keys()
93 .all(|k| matches!(k.as_str(), "error" | "status" | "fields"));
94 let (Some(detail), Some(status)) = (
95 old.get("error").and_then(|v| v.as_str()),
96 old.get("status").and_then(|v| v.as_u64()),
97 ) else {
98 return res;
99 };
100 if !known || status != u64::from(res.status.as_u16()) {
101 return res;
102 }
103
104 let reason = res.status.canonical_reason().unwrap_or("Error");
105 let slug = if old.contains_key("fields") {
106 "validation-failed".to_string()
107 } else {
108 reason.to_ascii_lowercase().replace(' ', "-")
109 };
110 let mut problem = serde_json::json!({
111 "type": match &self.type_base {
112 Some(base) => format!("{base}{slug}"),
113 None => "about:blank".to_string(),
114 },
115 "title": reason,
116 "status": status,
117 "detail": detail,
118 });
119 if self.include_instance {
120 problem["instance"] = path.into();
121 }
122 if let Some(fields) = old.get("fields") {
123 problem["errors"] = fields.clone();
124 }
125 let mut out = res;
126 out.body = serde_json::to_vec(&problem).unwrap_or_default().into();
127 out.headers.remove(http::header::CONTENT_LENGTH);
128 out.with_header("content-type", "application/problem+json")
129 }
130}
131
132#[async_trait]
133impl Middleware for ProblemDetails {
134 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
135 let path = req.uri.path().to_string();
136 let res = next.run(req).await.unwrap_or_else(Response::from_error);
137 Ok(self.convert(res, &path))
138 }
139}