graphql_starter/error/
api.rs1use std::collections::HashMap;
2
3use axum::{
4 http::StatusCode,
5 response::{IntoResponse, Response},
6};
7use error_info::ErrorInfo;
8use http::{header::IntoHeaderName, HeaderMap, HeaderValue};
9use serde::Serialize;
10
11use super::{Error, GenericErrorCode};
12use crate::axum::extract::Json;
13
14pub type ApiResult<T, E = Box<ApiError>> = std::result::Result<T, E>;
15
16#[derive(Debug, Serialize)]
18pub struct ApiError {
19 title: String,
21 #[serde(serialize_with = "serialize_status_u16")]
23 status: StatusCode,
24 detail: String,
26 #[serde(skip_serializing_if = "HashMap::is_empty")]
28 info: HashMap<String, String>,
29 #[serde(skip_serializing_if = "HashMap::is_empty")]
31 errors: HashMap<String, serde_json::Value>,
32 #[serde(skip)]
34 headers: Option<HeaderMap>,
35}
36
37impl ApiError {
38 pub fn new(status: StatusCode, detail: impl Into<String>) -> Box<Self> {
40 Box::new(ApiError {
41 title: status
42 .canonical_reason()
43 .unwrap_or(GenericErrorCode::InternalServerError.raw_message())
44 .to_owned(),
45 status,
46 detail: detail.into(),
47 info: Default::default(),
48 errors: Default::default(),
49 headers: None,
50 })
51 }
52
53 #[allow(clippy::boxed_local)]
55 pub fn from_err(err: Box<Error>) -> Box<Self> {
56 let err = *err;
57
58 if err.unexpected {
60 tracing::error!("{err:#}");
61 } else if tracing::event_enabled!(tracing::Level::DEBUG) {
62 tracing::warn!("{err:#}")
63 } else {
64 tracing::warn!("{err}")
65 }
66
67 let mut ret = ApiError::new(err.info.status(), err.info.message());
69
70 ret = ret.with_info("errorCode", err.info.code());
72 ret = ret.with_info("rawMessage", err.info.raw_message());
73 for (key, value) in err.info.fields() {
74 if key == "errorCode" || key == "rawMessage" {
75 tracing::error!("Error '{}' contains a reserved property: {}", err.info.code(), key);
76 continue;
77 }
78 ret = ret.with_info(key, value);
79 }
80
81 if let Some(properties) = err.properties {
83 for (key, value) in properties {
84 ret = ret.with_error_info(key, value);
85 }
86 }
87
88 ret
89 }
90
91 pub fn with_title(mut self: Box<Self>, title: impl Into<String>) -> Box<Self> {
93 self.title = title.into();
94 self
95 }
96
97 pub fn with_info(mut self: Box<Self>, key: impl Into<String>, value: impl Into<String>) -> Box<Self> {
99 self.info.insert(key.into(), value.into());
100 self
101 }
102
103 pub fn with_error_info(mut self: Box<Self>, field: impl Into<String>, info: serde_json::Value) -> Box<Self> {
105 self.errors.insert(field.into(), info);
106 self
107 }
108
109 pub fn with_header(mut self: Box<Self>, key: impl IntoHeaderName, value: impl TryInto<HeaderValue>) -> Box<Self> {
111 if let Ok(value) = value.try_into() {
112 let headers = self.headers.get_or_insert_with(Default::default);
113 headers.append(key, value);
114 }
115 self
116 }
117
118 pub fn title(&self) -> &str {
120 &self.title
121 }
122
123 pub fn status(&self) -> StatusCode {
125 self.status
126 }
127
128 pub fn detail(&self) -> &str {
130 &self.detail
131 }
132
133 pub fn info(&self) -> &HashMap<String, String> {
135 &self.info
136 }
137
138 pub fn errors(&self) -> &HashMap<String, serde_json::Value> {
140 &self.errors
141 }
142
143 pub fn headers(&self) -> &Option<HeaderMap> {
145 &self.headers
146 }
147}
148
149impl From<Box<Error>> for Box<ApiError> {
150 fn from(err: Box<Error>) -> Self {
151 ApiError::from_err(err)
152 }
153}
154
155impl IntoResponse for Box<ApiError> {
156 fn into_response(mut self) -> Response {
157 if let Some(headers) = self.headers.take() {
158 (self.status, headers, Json(self)).into_response()
159 } else {
160 (self.status, Json(self)).into_response()
161 }
162 }
163}
164
165fn serialize_status_u16<S>(status: &StatusCode, serializer: S) -> Result<S::Ok, S::Error>
166where
167 S: serde::Serializer,
168{
169 serializer.serialize_u16(status.as_u16())
170}