1use serde::{Deserialize, Serialize};
2
3use crate::error::CanonicalError;
4
5pub const APPLICATION_PROBLEM_JSON: &str = "application/problem+json";
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Problem {
14 #[serde(rename = "type")]
15 pub problem_type: String,
16 pub title: String,
17 pub status: u16,
18 pub detail: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub instance: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub trace_id: Option<String>,
23 pub context: serde_json::Value,
24}
25
26impl Problem {
27 pub fn from_error(err: &CanonicalError) -> Result<Self, serde_json::Error> {
36 let problem_type = format!("gts://{}", err.gts_type());
37 let title = err.title().to_owned();
38 let status = err.status_code();
39 let detail = err.detail().to_owned();
40
41 let mut context = serialize_context(err)?;
42
43 if let Some(rt) = err.resource_type() {
44 context["resource_type"] = serde_json::Value::String(rt.to_owned());
45 }
46
47 if let Some(rn) = err.resource_name() {
48 context["resource_name"] = serde_json::Value::String(rn.to_owned());
49 }
50
51 Ok(Problem {
52 problem_type,
53 title,
54 status,
55 detail,
56 instance: None,
57 trace_id: None,
58 context,
59 })
60 }
61
62 pub fn from_error_debug(err: &CanonicalError) -> Result<Self, serde_json::Error> {
77 let mut problem = Self::from_error(err)?;
78
79 if let Some(diag) = err.diagnostic() {
80 problem.context["description"] = serde_json::Value::String(diag.to_owned());
81 }
82
83 Ok(problem)
84 }
85
86 #[must_use]
88 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
89 self.trace_id = Some(trace_id.into());
90 self
91 }
92
93 #[must_use]
95 pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
96 self.instance = Some(instance.into());
97 self
98 }
99}
100
101fn serialize_context(err: &CanonicalError) -> Result<serde_json::Value, serde_json::Error> {
102 match err {
103 CanonicalError::Cancelled { ctx, .. } => serde_json::to_value(ctx),
104 CanonicalError::Unknown { ctx, .. } => serde_json::to_value(ctx),
105 CanonicalError::InvalidArgument { ctx, .. } => serde_json::to_value(ctx),
106 CanonicalError::DeadlineExceeded { ctx, .. } => serde_json::to_value(ctx),
107 CanonicalError::NotFound { ctx, .. } => serde_json::to_value(ctx),
108 CanonicalError::AlreadyExists { ctx, .. } => serde_json::to_value(ctx),
109 CanonicalError::PermissionDenied { ctx, .. } => serde_json::to_value(ctx),
110 CanonicalError::ResourceExhausted { ctx, .. } => serde_json::to_value(ctx),
111 CanonicalError::FailedPrecondition { ctx, .. } => serde_json::to_value(ctx),
112 CanonicalError::Aborted { ctx, .. } => serde_json::to_value(ctx),
113 CanonicalError::OutOfRange { ctx, .. } => serde_json::to_value(ctx),
114 CanonicalError::Unimplemented { ctx, .. } => serde_json::to_value(ctx),
115 CanonicalError::Internal { ctx, .. } => serde_json::to_value(ctx),
116 CanonicalError::ServiceUnavailable { ctx, .. } => serde_json::to_value(ctx),
117 CanonicalError::DataLoss { ctx, .. } => serde_json::to_value(ctx),
118 CanonicalError::Unauthenticated { ctx, .. } => serde_json::to_value(ctx),
119 }
120}
121
122impl From<CanonicalError> for Problem {
123 fn from(err: CanonicalError) -> Self {
124 match Problem::from_error(&err) {
125 Ok(p) => p,
126 Err(ser_err) => Problem {
127 problem_type: format!("gts://{}", err.gts_type()),
128 title: err.title().to_owned(),
129 status: err.status_code(),
130 detail: err.detail().to_owned(),
131 instance: None,
132 trace_id: None,
133 context: serde_json::Value::String(ser_err.to_string()),
134 },
135 }
136 }
137}
138
139#[cfg(feature = "axum")]
144impl axum::response::IntoResponse for Problem {
145 fn into_response(self) -> axum::response::Response {
146 match serde_json::to_vec(&self) {
147 Ok(body) => {
148 let status = http::StatusCode::from_u16(self.status)
149 .unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR);
150 (
151 status,
152 [(http::header::CONTENT_TYPE, APPLICATION_PROBLEM_JSON)],
153 body,
154 )
155 .into_response()
156 }
157 Err(e) => {
158 tracing::error!(
159 error = %e,
160 problem_type = %self.problem_type,
161 status = self.status,
162 "failed to serialize Problem; emitting fallback body",
163 );
164 let body: &[u8] = br#"{"type":"gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~","title":"Internal","status":500,"detail":"failed to serialize problem","context":{}}"#;
165 (
166 http::StatusCode::INTERNAL_SERVER_ERROR,
167 [(http::header::CONTENT_TYPE, APPLICATION_PROBLEM_JSON)],
168 body,
169 )
170 .into_response()
171 }
172 }
173 }
174}
175
176#[cfg(feature = "axum")]
177impl axum::response::IntoResponse for CanonicalError {
178 fn into_response(self) -> axum::response::Response {
179 Problem::from(self).into_response()
180 }
181}
182
183#[cfg(feature = "utoipa")]
188impl utoipa::PartialSchema for Problem {
189 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
190 use utoipa::openapi::schema::{KnownFormat, ObjectBuilder, SchemaFormat, SchemaType, Type};
191
192 ObjectBuilder::new()
193 .property(
194 "type",
195 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
196 )
197 .required("type")
198 .property(
199 "title",
200 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
201 )
202 .required("title")
203 .property(
204 "status",
205 ObjectBuilder::new()
206 .schema_type(SchemaType::Type(Type::Integer))
207 .format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32))),
208 )
209 .required("status")
210 .property(
211 "detail",
212 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
213 )
214 .required("detail")
215 .property(
216 "instance",
217 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
218 )
219 .property(
220 "trace_id",
221 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
222 )
223 .property(
224 "context",
225 ObjectBuilder::new().schema_type(SchemaType::Type(Type::Object)),
226 )
227 .required("context")
228 .description(Some(
229 "RFC 9457 problem+json. `context` varies by error category.",
230 ))
231 .into()
232 }
233}
234
235#[cfg(feature = "utoipa")]
236impl utoipa::ToSchema for Problem {
237 fn name() -> std::borrow::Cow<'static, str> {
238 std::borrow::Cow::Borrowed("Problem")
239 }
240}