upub_routes/
error.rs

1use axum::{http::StatusCode, response::Redirect};
2
3#[derive(Debug, thiserror::Error)]
4pub enum ApiError {
5	#[error("database error: {0:?}")]
6	Database(#[from] sea_orm::DbErr),
7
8	#[error("encountered malformed object: {0}")]
9	Malformed(#[from] apb::FieldErr),
10
11	#[error("http signature error: {0:?}")]
12	HttpSignature(#[from] httpsign::HttpSignatureError),
13
14	#[error("outgoing request error: {0:?}")]
15	Reqwest(#[from] reqwest::Error),
16
17	// TODO this is quite ugly because its basically a reqwest::Error but with extra string... buuut
18	// helps with debugging!
19	#[error("fetch error: {0:?}")]
20	FetchError(#[from] upub::traits::fetch::RequestError),
21
22	#[error("error interacting with file system: {0:?}")]
23	IO(#[from] std::io::Error),
24
25	// wrapper error to return arbitraty status codes
26	#[error("{0}")]
27	Status(StatusCode),
28
29	// TODO this isn't really an error but i need to redirect from some routes so this allows me to
30	// keep the type hints on the return type, still what the hell!!!!
31	#[error("redirecting to {0}")]
32	Redirect(String),
33}
34
35impl ApiError {
36	pub fn bad_request() -> Self {
37		Self::Status(axum::http::StatusCode::BAD_REQUEST)
38	}
39
40	pub fn unprocessable() -> Self {
41		Self::Status(axum::http::StatusCode::UNPROCESSABLE_ENTITY)
42	}
43
44	pub fn not_found() -> Self {
45		Self::Status(axum::http::StatusCode::NOT_FOUND)
46	}
47
48	pub fn forbidden() -> Self {
49		Self::Status(axum::http::StatusCode::FORBIDDEN)
50	}
51
52	pub fn unauthorized() -> Self {
53		Self::Status(axum::http::StatusCode::UNAUTHORIZED)
54	}
55
56	pub fn not_modified() -> Self {
57		Self::Status(axum::http::StatusCode::NOT_MODIFIED)
58	}
59
60	pub fn internal_server_error() -> Self {
61		Self::Status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
62	}
63}
64
65pub type ApiResult<T> = Result<T, ApiError>;
66
67impl From<axum::http::StatusCode> for ApiError {
68	fn from(value: axum::http::StatusCode) -> Self {
69		ApiError::Status(value)
70	}
71}
72
73impl axum::response::IntoResponse for ApiError {
74	fn into_response(self) -> axum::response::Response {
75		// TODO it's kind of jank to hide this print down here, i should probably learn how spans work
76		//      in tracing and use the library's features but ehhhh
77		tracing::debug!("emitting error response: {self:?}");
78		let descr = self.to_string();
79		match self {
80			ApiError::Redirect(to) => Redirect::to(&to).into_response(),
81			ApiError::Status(status) => status.into_response(),
82			ApiError::Database(e) => (
83				StatusCode::SERVICE_UNAVAILABLE,
84				axum::Json(serde_json::json!({
85					"error": "database",
86					"inner": format!("{e:#?}"),
87				}))
88			).into_response(),
89			ApiError::Reqwest(x) => (
90				x.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
91				axum::Json(serde_json::json!({
92					"error": "request",
93					"status": x.status().map(|s| s.to_string()).unwrap_or_default(),
94					"url": x.url().map(|x| x.to_string()).unwrap_or_default(),
95					"description": descr,
96					"inner": format!("{x:#?}"),
97				}))
98			).into_response(),
99			ApiError::FetchError(pull) => (
100				StatusCode::INTERNAL_SERVER_ERROR,
101				axum::Json(serde_json::json!({
102					"error": "fetch",
103					"description": descr,
104					"inner": format!("{pull:#?}"),
105				}))
106			).into_response(),
107			ApiError::Malformed(x) => (
108				axum::http::StatusCode::BAD_REQUEST,
109				axum::Json(serde_json::json!({
110					"error": "field",
111					"field": x.0.to_string(),
112					"description": descr,
113				}))
114			).into_response(),
115			x => (
116				StatusCode::INTERNAL_SERVER_ERROR,
117				axum::Json(serde_json::json!({
118					"error": "unknown",
119					"description": descr,
120					"inner": format!("{x:#?}"),
121				}))
122			).into_response(),
123		}
124	}
125}