Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ErrorKind::DeserializeError to specialize ErrorKind::Message (extract::path::ErrorKind) #2720

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

vegardgs-ksat
Copy link

Motivation

Expose the key and value of the extract::Path extrator for when the serde deserialization fails. This allow custom extractors wrapping the Path extractor to specialize their responses with more context about the failed operation. Currently, all failed deserializations end up as part of the extract::path::ErrorKind::Message variant.

I encountered this situation when attempting to create my own extrator, using it to deserialize uuid::Uuid resources, since I require specializing the response for these error conditions.

//! Manual implementation wrapping `axum::extract::Path` extrator.

use axum::extract::FromRequestParts;
use axum::extract::{path::ErrorKind, rejection::PathRejection};
use axum::http::{request::Parts, StatusCode};
use serde::de::DeserializeOwned;

pub struct Path<T>(pub T);

#[axum::async_trait]
impl<T, S> FromRequestParts<S> for Path<T>
where
    T: DeserializeOwned + Send,
    S: Send + Sync,
{
    type Rejection = (StatusCode, axum::Json<serde_json::Value>);

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        match axum::extract::Path::<T>::from_request_parts(parts, state).await {
            Ok(value) => Ok(Self(value.0)),
            Err(rejection) => match &rejection {
                PathRejection::FailedToDeserializePathParams(f) => match f.kind() {
                    ErrorKind::ParseErrorAtKey { key, value, .. } => Err((
                        StatusCode::BAD_REQUEST,
                        axum::Json(serde_json::json!({
                            "code": "MALFORMED_VALUE",
                            "message": format!("Value '{value}' could not be parsed"),
                            "target": key
                        })),
                    )),
                    
                    // Nominal UUID deserialization issuess occur in ErrorKind::Message
                    
                    _ => Err((
                        StatusCode::BAD_REQUEST,
                        axum::Json(serde_json::json!({
                            "code": "MALFORMED_VALUE",
                            "message": f.body_text(),
                            "target": "",
                        })),
                    )),
                },
                PathRejection::MissingPathParams(..) | _ => {
                    Err((
                        StatusCode::INTERNAL_SERVER_ERROR,
                        axum::Json(serde_json::json!({
                            "message": "An internal server error occurred",
                        })),
                    ))
                }
            },
        }
    }
}

// This is the endpoint handler using the custom Path extractor.
async fn find_resource(Path(resource_id): Path<uuid::Uuid>) -> String {
    todo!()
}

Solution

This commit introduces another extract::path::ErrorKind variant that captures the serde error nominally captured through the serde::de::Error trait impl on PathDeserializeError. We augment the deserialization error with the captured (key, value), allowing extract::Path, and wrapping extractors, to gain programmatic access to the key name, and attempted deserialized value.

This allows me to expand the handling in the wrapping Path extractor.

ErrorKind::DeserializeError { key, value, message } => Err((StatusCode::BAD_REQUEST, axum::Json(serde_json::json!({
  "code": "MALFORMED_VALUE",
  "message": format!("Value '{value}' could not be parsed"),
  "target": key
})),

Copy link
Contributor

@mladedav mladedav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, just a few minor points.

key,
value,
message,
} => write!(f, "Cannot parse `{key}` with value `{value:?}`: {message}"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use debug for value, it would add redundant quotes. You can also that in the new tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just mirroring the practice established by the other formats for the other variants:

ErrorKind::ParseErrorAtKey {
key,
value,
expected_type,
} => write!(
f,
"Cannot parse `{key}` with value `{value:?}` to a `{expected_type}`"
),
ErrorKind::ParseError {
value,
expected_type,
} => write!(f, "Cannot parse `{value:?}` to a `{expected_type}`"),
ErrorKind::ParseErrorAtIndex {
index,
value,
expected_type,
} => write!(
f,
"Cannot parse value at index {index} with value `{value:?}` to a `{expected_type}`"
),

I was a bit perplexed about this as well, and thought it did not make sense to use debug format for value.

Should I change it do non-Debug for this new variant only, or change the other variants as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an additional commit that changes the format for all variants.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Sorry I didn't notice you just wrote it consistently with the rest, otherwise I wouldn't bring it up here.

axum/src/extract/path/de.rs Outdated Show resolved Hide resolved
axum/src/extract/path/de.rs Outdated Show resolved Hide resolved
axum/src/extract/path/de.rs Outdated Show resolved Hide resolved
…rorKind::Message

This commit introduces another `extract::path::ErrorKind` variant that captures the
serde error nominally captured through the `serde::de::Error` trait impl on `PathDeserializeError`.
We augment the deserialization error with the captured (key, value), allowing `extract::Path`, and wrapping
extractors, to gain programmatic access to the key name, and attempted deserialized value.

The `PathDeserializationError::custom` is used two places in addition to capture the deserialization error.
These usages should still be unaffected.
@mladedav
Copy link
Contributor

LGTM, @jplatte can you take a look when you have some free time?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants