1use serde::{Deserialize, Serialize};
2
3use crate::context::{
4 Aborted, AlreadyExists, Cancelled, DataLoss, DeadlineExceeded, FailedPrecondition, Internal,
5 InvalidArgument, NotFound, OutOfRange, PermissionDenied, ResourceExhausted, ServiceUnavailable,
6 Unauthenticated, Unimplemented, Unknown,
7};
8use crate::error::CanonicalError;
9
10pub const APPLICATION_PROBLEM_JSON: &str = "application/problem+json";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Problem {
19 #[serde(rename = "type")]
20 pub problem_type: String,
21 pub title: String,
22 pub status: u16,
23 pub detail: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub instance: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub trace_id: Option<String>,
28 pub context: serde_json::Value,
29}
30
31impl Problem {
32 pub fn from_error(err: &CanonicalError) -> Result<Self, serde_json::Error> {
41 let problem_type = format!("gts://{}", err.gts_type());
42 let title = err.title().to_owned();
43 let status = err.status_code();
44 let detail = err.detail().to_owned();
45
46 let mut context = serialize_context(err)?;
47
48 if let Some(rt) = err.resource_type() {
49 context["resource_type"] = serde_json::Value::String(rt.to_owned());
50 }
51
52 if let Some(rn) = err.resource_name() {
53 context["resource_name"] = serde_json::Value::String(rn.to_owned());
54 }
55
56 Ok(Problem {
57 problem_type,
58 title,
59 status,
60 detail,
61 instance: None,
62 trace_id: None,
63 context,
64 })
65 }
66
67 pub fn from_error_debug(err: &CanonicalError) -> Result<Self, serde_json::Error> {
82 let mut problem = Self::from_error(err)?;
83
84 if let Some(diag) = err.diagnostic() {
85 problem.context["description"] = serde_json::Value::String(diag.to_owned());
86 }
87
88 Ok(problem)
89 }
90
91 #[must_use]
93 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
94 self.trace_id = Some(trace_id.into());
95 self
96 }
97
98 #[must_use]
100 pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
101 self.instance = Some(instance.into());
102 self
103 }
104}
105
106fn serialize_context(err: &CanonicalError) -> Result<serde_json::Value, serde_json::Error> {
107 match err {
108 CanonicalError::Cancelled { ctx, .. } => serde_json::to_value(ctx),
109 CanonicalError::Unknown { ctx, .. } => serde_json::to_value(ctx),
110 CanonicalError::InvalidArgument { ctx, .. } => serde_json::to_value(ctx),
111 CanonicalError::DeadlineExceeded { ctx, .. } => serde_json::to_value(ctx),
112 CanonicalError::NotFound { ctx, .. } => serde_json::to_value(ctx),
113 CanonicalError::AlreadyExists { ctx, .. } => serde_json::to_value(ctx),
114 CanonicalError::PermissionDenied { ctx, .. } => serde_json::to_value(ctx),
115 CanonicalError::ResourceExhausted { ctx, .. } => serde_json::to_value(ctx),
116 CanonicalError::FailedPrecondition { ctx, .. } => serde_json::to_value(ctx),
117 CanonicalError::Aborted { ctx, .. } => serde_json::to_value(ctx),
118 CanonicalError::OutOfRange { ctx, .. } => serde_json::to_value(ctx),
119 CanonicalError::Unimplemented { ctx, .. } => serde_json::to_value(ctx),
120 CanonicalError::Internal { ctx, .. } => serde_json::to_value(ctx),
121 CanonicalError::ServiceUnavailable { ctx, .. } => serde_json::to_value(ctx),
122 CanonicalError::DataLoss { ctx, .. } => serde_json::to_value(ctx),
123 CanonicalError::Unauthenticated { ctx, .. } => serde_json::to_value(ctx),
124 }
125}
126
127#[allow(unknown_lints, de1302_error_from_to_string)]
131impl From<CanonicalError> for Problem {
132 fn from(err: CanonicalError) -> Self {
133 match Problem::from_error(&err) {
134 Ok(p) => p,
135 Err(ser_err) => Problem {
136 problem_type: format!("gts://{}", err.gts_type()),
137 title: err.title().to_owned(),
138 status: err.status_code(),
139 detail: err.detail().to_owned(),
140 instance: None,
141 trace_id: None,
142 context: serde_json::Value::String(ser_err.to_string()),
143 },
144 }
145 }
146}
147
148const PROBLEM_TYPE_PREFIX: &str = "gts://";
168
169#[derive(Debug, thiserror::Error)]
171pub enum ProblemConversionError {
172 #[error("unrecognized problem_type: {0}")]
176 UnknownProblemType(String),
177
178 #[error("invalid context for canonical category {category}: {source}")]
182 InvalidContext {
183 category: &'static str,
184 #[source]
185 source: serde_json::Error,
186 },
187}
188
189#[allow(unknown_lints, de0901_gts_string_pattern)]
194const GTS_TYPE_PREFIX: &str = "gts.cf.core.errors.err.v1~cf.core.err.";
195const GTS_TYPE_SUFFIX: &str = ".v1~";
197
198fn category_from_problem_type(problem_type: &str) -> Option<&str> {
201 let rest = problem_type.strip_prefix(PROBLEM_TYPE_PREFIX)?;
202 let after_prefix = rest.strip_prefix(GTS_TYPE_PREFIX)?;
203 after_prefix.strip_suffix(GTS_TYPE_SUFFIX)
204}
205
206fn extract_resource_fields(context: &serde_json::Value) -> (Option<String>, Option<String>) {
212 let resource_type = context
213 .get("resource_type")
214 .and_then(serde_json::Value::as_str)
215 .map(str::to_owned);
216 let resource_name = context
217 .get("resource_name")
218 .and_then(serde_json::Value::as_str)
219 .map(str::to_owned);
220 (resource_type, resource_name)
221}
222
223fn deserialize_ctx<T>(
224 category: &'static str,
225 context: serde_json::Value,
226) -> Result<T, ProblemConversionError>
227where
228 T: serde::de::DeserializeOwned,
229{
230 serde_json::from_value(context)
231 .map_err(|source| ProblemConversionError::InvalidContext { category, source })
232}
233
234impl TryFrom<Problem> for CanonicalError {
235 type Error = ProblemConversionError;
236
237 fn try_from(problem: Problem) -> Result<Self, Self::Error> {
238 let category = category_from_problem_type(&problem.problem_type).ok_or_else(|| {
239 ProblemConversionError::UnknownProblemType(problem.problem_type.clone())
240 })?;
241
242 let detail = problem.detail;
243 let (resource_type, resource_name) = extract_resource_fields(&problem.context);
244 let ctx_value = problem.context;
245
246 let canonical = match category {
247 "cancelled" => Self::Cancelled {
248 ctx: deserialize_ctx::<Cancelled>("cancelled", ctx_value)?,
249 detail,
250 resource_type,
251 resource_name,
252 },
253 "unknown" => Self::Unknown {
254 ctx: deserialize_ctx::<Unknown>("unknown", ctx_value)?,
255 detail,
256 resource_type,
257 resource_name,
258 },
259 "invalid_argument" => Self::InvalidArgument {
260 ctx: deserialize_ctx::<InvalidArgument>("invalid_argument", ctx_value)?,
261 detail,
262 resource_type,
263 resource_name,
264 },
265 "deadline_exceeded" => Self::DeadlineExceeded {
266 ctx: deserialize_ctx::<DeadlineExceeded>("deadline_exceeded", ctx_value)?,
267 detail,
268 resource_type,
269 resource_name,
270 },
271 "not_found" => Self::NotFound {
272 ctx: deserialize_ctx::<NotFound>("not_found", ctx_value)?,
273 detail,
274 resource_type,
275 resource_name,
276 },
277 "already_exists" => Self::AlreadyExists {
278 ctx: deserialize_ctx::<AlreadyExists>("already_exists", ctx_value)?,
279 detail,
280 resource_type,
281 resource_name,
282 },
283 "permission_denied" => Self::PermissionDenied {
284 ctx: deserialize_ctx::<PermissionDenied>("permission_denied", ctx_value)?,
285 detail,
286 resource_type,
287 resource_name,
288 },
289 "resource_exhausted" => Self::ResourceExhausted {
290 ctx: deserialize_ctx::<ResourceExhausted>("resource_exhausted", ctx_value)?,
291 detail,
292 resource_type,
293 resource_name,
294 },
295 "failed_precondition" => Self::FailedPrecondition {
296 ctx: deserialize_ctx::<FailedPrecondition>("failed_precondition", ctx_value)?,
297 detail,
298 resource_type,
299 resource_name,
300 },
301 "aborted" => Self::Aborted {
302 ctx: deserialize_ctx::<Aborted>("aborted", ctx_value)?,
303 detail,
304 resource_type,
305 resource_name,
306 },
307 "out_of_range" => Self::OutOfRange {
308 ctx: deserialize_ctx::<OutOfRange>("out_of_range", ctx_value)?,
309 detail,
310 resource_type,
311 resource_name,
312 },
313 "unimplemented" => Self::Unimplemented {
314 ctx: deserialize_ctx::<Unimplemented>("unimplemented", ctx_value)?,
315 detail,
316 resource_type,
317 resource_name,
318 },
319 "internal" => Self::Internal {
320 ctx: deserialize_ctx::<Internal>("internal", ctx_value)?,
323 detail,
324 },
325 "service_unavailable" => Self::ServiceUnavailable {
326 ctx: deserialize_ctx::<ServiceUnavailable>("service_unavailable", ctx_value)?,
327 detail,
328 resource_type,
329 resource_name,
330 },
331 "data_loss" => Self::DataLoss {
332 ctx: deserialize_ctx::<DataLoss>("data_loss", ctx_value)?,
333 detail,
334 resource_type,
335 resource_name,
336 },
337 "unauthenticated" => Self::Unauthenticated {
338 ctx: deserialize_ctx::<Unauthenticated>("unauthenticated", ctx_value)?,
339 detail,
340 resource_type,
341 resource_name,
342 },
343 _ => {
344 return Err(ProblemConversionError::UnknownProblemType(
345 problem.problem_type,
346 ));
347 }
348 };
349
350 Ok(canonical)
351 }
352}
353
354#[cfg(feature = "axum")]
359impl axum::response::IntoResponse for Problem {
360 fn into_response(self) -> axum::response::Response {
361 match serde_json::to_vec(&self) {
362 Ok(body) => {
363 let status = http::StatusCode::from_u16(self.status)
364 .unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR);
365 (
366 status,
367 [(http::header::CONTENT_TYPE, APPLICATION_PROBLEM_JSON)],
368 body,
369 )
370 .into_response()
371 }
372 Err(e) => {
373 tracing::error!(
374 error = %e,
375 problem_type = %self.problem_type,
376 status = self.status,
377 "failed to serialize Problem; emitting fallback body",
378 );
379 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":{}}"#;
380 (
381 http::StatusCode::INTERNAL_SERVER_ERROR,
382 [(http::header::CONTENT_TYPE, APPLICATION_PROBLEM_JSON)],
383 body,
384 )
385 .into_response()
386 }
387 }
388 }
389}
390
391#[cfg(feature = "axum")]
392impl axum::response::IntoResponse for CanonicalError {
393 fn into_response(self) -> axum::response::Response {
394 let for_extension = self.clone();
401 let mut response = Problem::from(self).into_response();
402 response.extensions_mut().insert(for_extension);
403 response
404 }
405}
406
407#[cfg(feature = "utoipa")]
412impl utoipa::PartialSchema for Problem {
413 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
414 use utoipa::openapi::schema::{KnownFormat, ObjectBuilder, SchemaFormat, SchemaType, Type};
415
416 ObjectBuilder::new()
417 .property(
418 "type",
419 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
420 )
421 .required("type")
422 .property(
423 "title",
424 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
425 )
426 .required("title")
427 .property(
428 "status",
429 ObjectBuilder::new()
430 .schema_type(SchemaType::Type(Type::Integer))
431 .format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32))),
432 )
433 .required("status")
434 .property(
435 "detail",
436 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
437 )
438 .required("detail")
439 .property(
440 "instance",
441 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
442 )
443 .property(
444 "trace_id",
445 ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)),
446 )
447 .property(
448 "context",
449 ObjectBuilder::new().schema_type(SchemaType::Type(Type::Object)),
450 )
451 .required("context")
452 .description(Some(
453 "RFC 9457 problem+json. `context` varies by error category.",
454 ))
455 .into()
456 }
457}
458
459#[cfg(feature = "utoipa")]
460impl utoipa::ToSchema for Problem {
461 fn name() -> std::borrow::Cow<'static, str> {
462 std::borrow::Cow::Borrowed("Problem")
463 }
464}