1use std::borrow::Cow;
11use std::collections::HashMap;
12use std::fmt::{self, Display};
13use std::io;
14
15use std::error::Error;
16
17use http::Extensions;
18use serde::Serialize;
19use serde_json::Value;
20
21use super::*;
22pub use http_api_problem_derive::IntoApiError;
23
24pub struct ApiErrorBuilder {
25 pub status: StatusCode,
27
28 pub title: Option<String>,
31
32 pub message: Option<String>,
36
37 pub type_url: Option<String>,
39
40 pub instance: Option<String>,
44
45 pub fields: HashMap<String, Value>,
48
49 pub extensions: Extensions,
55
56 pub source: Option<Box<dyn Error + Send + Sync + 'static>>,
57}
58
59impl ApiErrorBuilder {
60 pub fn status<T: Into<StatusCode>>(mut self, status: T) -> Self {
62 self.status = status.into();
63 self
64 }
65
66 pub fn try_status<T: TryInto<StatusCode>>(self, status: T) -> Result<Self, InvalidStatusCode>
70 where
71 T::Error: Into<InvalidStatusCode>,
72 {
73 let status = status.try_into().map_err(|e| e.into())?;
74 Ok(self.status(status))
75 }
76
77 pub fn title<T: Display>(mut self, title: T) -> Self {
80 self.title = Some(title.to_string());
81 self
82 }
83
84 pub fn message<M: Display>(mut self, message: M) -> Self {
88 self.message = Some(message.to_string());
89 self
90 }
91
92 pub fn type_url<U: Display>(mut self, type_url: U) -> Self {
94 self.type_url = Some(type_url.to_string());
95 self
96 }
97
98 pub fn instance<T: Display>(mut self, instance: T) -> Self {
104 self.instance = Some(instance.to_string());
105 self
106 }
107
108 pub fn field<T: Into<String>, V: Serialize>(mut self, name: T, value: V) -> Self {
113 if let Ok(value) = serde_json::to_value(value) {
114 self.fields.insert(name.into(), value);
115 }
116
117 self
118 }
119
120 pub fn with_fields<F>(mut self, f: F) -> Self
122 where
123 F: FnOnce(HashMap<String, Value>) -> HashMap<String, Value>,
124 {
125 self.fields = f(self.fields);
126
127 self
128 }
129
130 pub fn extension<T: Send + Sync + Clone + 'static>(mut self, val: T) -> Self {
136 let _ = self.extensions.insert(val);
137
138 self
139 }
140
141 pub fn with_extensions<F>(mut self, f: F) -> Self
145 where
146 F: FnOnce(Extensions) -> Extensions,
147 {
148 self.extensions = f(self.extensions);
149
150 self
151 }
152
153 pub fn source<E: Error + Send + Sync + 'static>(self, source: E) -> Self {
154 self.source_in_a_box(Box::new(source))
155 }
156
157 pub fn source_in_a_box<E: Into<Box<dyn Error + Send + Sync + 'static>>>(
158 mut self,
159 source: E,
160 ) -> Self {
161 self.source = Some(source.into());
162 self
163 }
164
165 pub fn finish(self) -> ApiError {
167 ApiError {
168 status: self.status,
169 title: self.title,
170 message: self.message,
171 type_url: self.type_url,
172 instance: self.instance,
173 fields: self.fields,
174 extensions: self.extensions,
175 source: self.source,
176 }
177 }
178}
179
180#[derive(Debug)]
199pub struct ApiError {
200 status: StatusCode,
201 title: Option<String>,
202 message: Option<String>,
203 instance: Option<String>,
204 type_url: Option<String>,
205 fields: HashMap<String, Value>,
206 extensions: Extensions,
207 source: Option<Box<dyn Error + Send + Sync + 'static>>,
208}
209
210impl ApiError {
211 pub fn builder<T: Into<StatusCode>>(status: T) -> ApiErrorBuilder {
213 ApiErrorBuilder {
214 status: status.into(),
215 title: None,
216 message: None,
217 type_url: None,
218 instance: None,
219 fields: HashMap::default(),
220 source: None,
221 extensions: Extensions::default(),
222 }
223 }
224
225 pub fn try_builder<S: TryInto<StatusCode>>(
229 status: S,
230 ) -> Result<ApiErrorBuilder, InvalidStatusCode>
231 where
232 S::Error: Into<InvalidStatusCode>,
233 {
234 let status = status.try_into().map_err(|e| e.into())?;
235 Ok(Self::builder(status))
236 }
237
238 pub fn new<T: Into<StatusCode>>(status: T) -> Self {
240 Self {
241 status: status.into(),
242 title: None,
243 message: None,
244 type_url: None,
245 instance: None,
246 fields: HashMap::new(),
247 extensions: Extensions::default(),
248 source: None,
249 }
250 }
251
252 pub fn try_new<S: TryInto<StatusCode>>(status: S) -> Result<Self, InvalidStatusCode>
256 where
257 S::Error: Into<InvalidStatusCode>,
258 {
259 let status = status.try_into().map_err(|e| e.into())?;
260 Ok(Self::new(status))
261 }
262
263 pub fn set_status<T: Into<StatusCode>>(&mut self, status: T) {
265 self.status = status.into();
266 }
267
268 pub fn status(&self) -> StatusCode {
270 self.status
271 }
272
273 pub fn set_title<T: Display>(&mut self, title: T) {
276 self.title = Some(title.to_string())
277 }
278
279 pub fn title(&self) -> Option<&str> {
282 self.title.as_deref()
283 }
284
285 pub fn set_message<T: Display>(&mut self, message: T) {
287 self.message = Some(message.to_string())
288 }
289
290 pub fn message(&self) -> Option<&str> {
292 self.message.as_deref()
293 }
294
295 pub fn set_type_url<T: Display>(&mut self, type_url: T) {
300 self.type_url = Some(type_url.to_string())
301 }
302
303 pub fn type_url(&self) -> Option<&str> {
305 self.type_url.as_deref()
306 }
307
308 pub fn set_instance<T: Display>(&mut self, instance: T) {
309 self.instance = Some(instance.to_string())
310 }
311
312 pub fn instance(&self) -> Option<&str> {
314 self.instance.as_deref()
315 }
316
317 pub fn set_source<E: Error + Send + Sync + 'static>(&mut self, source: E) {
318 self.set_source_in_a_box(Box::new(source))
319 }
320
321 pub fn set_source_in_a_box<E: Into<Box<dyn Error + Send + Sync + 'static>>>(
322 &mut self,
323 source: E,
324 ) {
325 self.source = Some(source.into());
326 }
327
328 pub fn add_field<T: Into<String>, V: Serialize>(&mut self, name: T, value: V) -> bool {
334 self.try_add_field(name, value).is_ok()
335 }
336
337 pub fn try_add_field<T: Into<String>, V: Serialize>(
342 &mut self,
343 name: T,
344 value: V,
345 ) -> Result<(), Box<dyn Error + 'static>> {
346 let name: String = name.into();
347
348 match name.as_ref() {
349 "type" => return Err("'type' is a reserved field name".into()),
350 "status" => return Err("'status' is a reserved field name".into()),
351 "title" => return Err("'title' is a reserved field name".into()),
352 "detail" => return Err("'detail' is a reserved field name".into()),
353 "instance" => return Err("'instance' is a reserved field name".into()),
354 _ => (),
355 }
356
357 match serde_json::to_value(value) {
358 Ok(value) => {
359 self.fields.insert(name, value);
360 Ok(())
361 }
362 Err(err) => Err(Box::new(err)),
363 }
364 }
365
366 pub fn fields(&self) -> &HashMap<String, Value> {
368 &self.fields
369 }
370
371 pub fn fields_mut(&mut self) -> &mut HashMap<String, Value> {
373 &mut self.fields
374 }
375
376 pub fn extensions(&self) -> &Extensions {
380 &self.extensions
381 }
382
383 pub fn extensions_mut(&mut self) -> &mut Extensions {
387 &mut self.extensions
388 }
389
390 pub fn to_http_api_problem(&self) -> HttpApiProblem {
395 let mut problem = HttpApiProblem::with_title_and_type(self.status);
396
397 problem.title.clone_from(&self.title);
398
399 if let Some(message) = self.detail_message() {
400 problem.detail = Some(message.into())
401 }
402
403 problem.type_url.clone_from(&self.type_url);
404 problem.instance.clone_from(&self.instance);
405
406 if self.status != StatusCode::UNAUTHORIZED {
407 for (key, value) in self.fields.iter() {
408 problem.set_value(key.to_string(), value);
409 }
410 }
411
412 problem
413 }
414
415 pub fn into_http_api_problem(self) -> HttpApiProblem {
420 let mut problem = HttpApiProblem::with_title_and_type(self.status);
421
422 if let Some(title) = self.title.as_ref() {
423 problem.title = Some(title.to_owned());
424 }
425
426 if let Some(message) = self.detail_message() {
427 problem.detail = Some(message.into())
428 }
429
430 if let Some(type_url) = self.type_url.as_ref() {
431 problem.type_url = Some(type_url.to_owned())
432 }
433
434 if let Some(instance) = self.instance.as_ref() {
435 problem.instance = Some(instance.to_owned())
436 }
437
438 if self.status != StatusCode::UNAUTHORIZED {
439 for (key, value) in self.fields.iter() {
440 problem.set_value(key.to_string(), value);
441 }
442 }
443
444 problem
445 }
446
447 pub fn detail_message(&self) -> Option<Cow<str>> {
451 if let Some(message) = self.message.as_ref() {
452 return Some(Cow::Borrowed(message));
453 }
454
455 if let Some(source) = self.source() {
456 return Some(Cow::Owned(source.to_string()));
457 }
458
459 None
460 }
461
462 #[cfg(feature = "hyper")]
466 pub fn into_hyper_response(self) -> hyper::Response<String> {
467 let problem = self.into_http_api_problem();
468 problem.to_hyper_response()
469 }
470
471 #[cfg(feature = "axum")]
475 pub fn into_axum_response(self) -> axum_core::response::Response {
476 let problem = self.into_http_api_problem();
477 problem.to_axum_response()
478 }
479
480 #[cfg(feature = "actix-web")]
484 pub fn into_actix_web_response(self) -> actix_web::HttpResponse {
485 let problem = self.into_http_api_problem();
486 problem.into()
487 }
488
489 #[cfg(feature = "salvo")]
493 pub fn into_salvo_response(self) -> salvo::Response {
494 let problem = self.into_http_api_problem();
495 problem.to_salvo_response()
496 }
497
498 #[cfg(feature = "tide")]
502 pub fn into_tide_response(self) -> tide::Response {
503 let problem = self.into_http_api_problem();
504 problem.to_tide_response()
505 }
506}
507
508impl Error for ApiError {
509 fn source(&self) -> Option<&(dyn Error + 'static)> {
510 self.source.as_ref().map(|e| &**e as _)
511 }
512}
513
514impl Display for ApiError {
515 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
516 write!(f, "{}", self.status)?;
517
518 match (self.title.as_ref(), self.detail_message()) {
519 (Some(title), Some(detail)) => return write!(f, " - {} - {}", title, detail),
520 (Some(title), None) => return write!(f, " - {}", title),
521 (None, Some(detail)) => return write!(f, " - {}", detail),
522 (None, None) => (),
523 }
524
525 if let Some(type_url) = self.type_url.as_ref() {
526 return write!(f, " of type {}", type_url);
527 }
528
529 if let Some(instance) = self.instance.as_ref() {
530 return write!(f, " on {}", instance);
531 }
532
533 Ok(())
534 }
535}
536
537impl From<StatusCode> for ApiError {
538 fn from(s: StatusCode) -> Self {
539 Self::new(s)
540 }
541}
542
543impl From<ApiErrorBuilder> for ApiError {
544 fn from(builder: ApiErrorBuilder) -> Self {
545 builder.finish()
546 }
547}
548
549impl From<ApiError> for HttpApiProblem {
550 fn from(error: ApiError) -> Self {
551 error.into_http_api_problem()
552 }
553}
554
555impl From<io::Error> for ApiError {
556 fn from(error: io::Error) -> Self {
557 ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
558 .title("An IO error occurred")
559 .source(error)
560 .finish()
561 }
562}
563
564impl From<std::convert::Infallible> for ApiError {
565 fn from(error: std::convert::Infallible) -> Self {
566 match error {}
567 }
568}
569
570pub trait IntoApiError {
571 fn into_api_error(self) -> ApiError;
572}
573
574impl<T: IntoApiError> From<T> for ApiError {
575 fn from(t: T) -> ApiError {
576 t.into_api_error()
577 }
578}
579
580#[cfg(feature = "hyper")]
581impl From<hyper::Error> for ApiError {
582 fn from(error: hyper::Error) -> Self {
583 ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
584 .source(error)
585 .finish()
586 }
587}
588
589#[cfg(feature = "hyper")]
590impl From<ApiError> for hyper::Response<String> {
591 fn from(error: ApiError) -> hyper::Response<String> {
592 error.into_hyper_response()
593 }
594}
595
596#[cfg(feature = "axum")]
597impl From<ApiError> for axum_core::response::Response {
598 fn from(error: ApiError) -> axum_core::response::Response {
599 error.into_axum_response()
600 }
601}
602
603#[cfg(feature = "axum")]
604impl axum_core::response::IntoResponse for ApiError {
605 fn into_response(self) -> axum_core::response::Response {
606 self.into()
607 }
608}
609
610#[cfg(feature = "actix-web")]
611impl From<actix::prelude::MailboxError> for ApiError {
612 fn from(error: actix::prelude::MailboxError) -> Self {
613 ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
614 .source(error)
615 .finish()
616 }
617}
618
619#[cfg(feature = "actix-web")]
620impl From<ApiError> for actix_web::HttpResponse {
621 fn from(error: ApiError) -> Self {
622 error.into_actix_web_response()
623 }
624}
625
626#[cfg(feature = "actix-web")]
627impl actix_web::error::ResponseError for ApiError {
628 fn error_response(&self) -> actix_web::HttpResponse {
629 let json = self.to_http_api_problem().json_bytes();
630 let actix_status = actix_web::http::StatusCode::from_u16(self.status.as_u16())
631 .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
632
633 actix_web::HttpResponse::build(actix_status)
634 .append_header((
635 actix_web::http::header::CONTENT_TYPE,
636 PROBLEM_JSON_MEDIA_TYPE,
637 ))
638 .body(json)
639 }
640}
641
642#[cfg(feature = "warp")]
643impl warp::reject::Reject for ApiError {}
644
645#[cfg(feature = "salvo")]
646impl From<salvo::Error> for ApiError {
647 fn from(error: salvo::Error) -> Self {
648 ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
649 .source(error)
650 .finish()
651 }
652}
653
654#[cfg(feature = "salvo")]
655impl From<ApiError> for salvo::Response {
656 fn from(error: ApiError) -> salvo::Response {
657 error.into_salvo_response()
658 }
659}
660
661#[cfg(feature = "tide")]
662impl From<tide::Error> for ApiError {
663 fn from(error: tide::Error) -> Self {
664 let status: StatusCode = u16::from(error.status())
667 .try_into()
668 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
669 ApiError::builder(status)
670 .source_in_a_box(error.into_inner())
671 .finish()
672 }
673}
674
675#[cfg(feature = "tide")]
676impl From<ApiError> for tide::Response {
677 fn from(error: ApiError) -> tide::Response {
678 error.into_tide_response()
679 }
680}