Skip to content

Instantly share code, notes, and snippets.

@kyldvs
Last active April 14, 2024 14:04
Show Gist options
  • Save kyldvs/1025d81dac85332175066ac839c41d44 to your computer and use it in GitHub Desktop.
Save kyldvs/1025d81dac85332175066ac839c41d44 to your computer and use it in GitHub Desktop.
Axum Json Errors
use axum::{
async_trait,
extract::{
FromRequest,
FromRequestParts,
},
http::{
request::Parts,
StatusCode,
},
};
use serde::{
de::DeserializeOwned,
Deserialize,
Serialize,
};
use server::{
response::Response,
status,
};
// Define our own `Body` extractor to customize the rejection.
// See: https://github.com/tokio-rs/axum/blob/main/examples/customize-extractor-error/src/derive_from_request.rs
#[derive(Deserialize, Serialize, FromRequest, Debug, Clone, Copy, Default)]
#[from_request(via(axum::Json), rejection(Response))]
pub struct Body<T>(pub T);
// Define our own `Query` extractor to customize the rejection.
// From: https://github.com/tokio-rs/axum/blob/main/axum-extra/src/extract/query.rs#L93
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct Query<T>(pub T);
#[async_trait]
impl<T, S> FromRequestParts<S> for Query<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
let query = parts.uri.query().unwrap_or_default();
let value = serde_html_form::from_str(query);
let result = match value {
Ok(value) => Ok(Query(value)),
Err(err) => {
let code = StatusCode::BAD_REQUEST;
let res = Response::error_with_help(
code,
status::get_message(code),
format!("Invalid Query Parameters: {}", err).as_str(),
);
Err(res)
}
};
result
}
}
// Also wrap Path, and other relevant extractors approriately.
use hyper::StatusCode;
#[derive(Debug)]
pub struct Response {
code: StatusCode,
body: Value,
}
impl Response {
pub fn ok(data: impl Into<Value>) -> Self {
Self {
code: StatusCode::OK,
body: json!({
"success": true,
"data": data.into(),
}),
}
}
pub fn error(code: StatusCode, message: &str) -> Self {
Self {
code,
body: json!({
"success": false,
"message": message,
}),
}
}
pub fn error_with_help(code: StatusCode, message: &str, help: &str) -> Self {
Self {
code,
body: json!({
"success": false,
"message": message,
"help": help,
}),
}
}
}
impl From<StatusCode> for Response {
fn from(value: StatusCode) -> Self {
if value.is_success() {
Response::ok(json!(null))
} else {
Response::error(value, status::get_message(value))
}
}
}
impl IntoResponse for Response {
fn into_response(self) -> axum::response::Response {
(self.code, Json(self.body)).into_response()
}
}
// Define all the rejections from axum::extract::rejection.
macro_rules! rejection {
($a:path) => {
impl From<$a> for Response {
fn from(rejection: $a) -> Self {
rejection.status().into()
}
}
};
}
rejection!(axum::extract::rejection::JsonDataError);
rejection!(axum::extract::rejection::JsonSyntaxError);
rejection!(axum::extract::rejection::MissingJsonContentType);
rejection!(axum::extract::rejection::MissingExtension);
rejection!(axum::extract::rejection::MissingPathParams);
rejection!(axum::extract::rejection::InvalidFormContentType);
rejection!(axum::extract::rejection::FailedToResolveHost);
rejection!(axum::extract::rejection::FailedToDeserializeForm);
rejection!(axum::extract::rejection::FailedToDeserializeFormBody);
rejection!(axum::extract::rejection::FailedToDeserializeQueryString);
rejection!(axum::extract::rejection::QueryRejection);
rejection!(axum::extract::rejection::FormRejection);
rejection!(axum::extract::rejection::RawFormRejection);
rejection!(axum::extract::rejection::JsonRejection);
rejection!(axum::extract::rejection::ExtensionRejection);
rejection!(axum::extract::rejection::PathRejection);
rejection!(axum::extract::rejection::RawPathParamsRejection);
rejection!(axum::extract::rejection::HostRejection);
rejection!(axum::extract::rejection::MatchedPathMissing);
rejection!(axum::extract::rejection::NestedPathRejection);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment