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 3 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
Collaborator

@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
Collaborator

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
Collaborator

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
@vegardgs-ksat vegardgs-ksat force-pushed the add-deserialize-error branch 2 times, most recently from 85e31f3 to 53e8e14 Compare April 25, 2024 08:57
@mladedav
Copy link
Collaborator

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

@vegardgs-ksat
Copy link
Author

Rebased against latest main for an up-to-date PR.

@vegardgs-ksat
Copy link
Author

@mladedav anything holding up this PR?

Copy link
Member

@jplatte jplatte left a comment

Choose a reason for hiding this comment

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

Yeah, I was holding it up by not reviewing, sorry. I did so now, and the change looks good to me.

One question, and a request though:

  • Is ErrorKind::Message still constructed anywhere? If not, would make sense to remove it, I wouldn't want to release this in a patch of 0.7 anyways.
  • Could you please add a changelog entry?

vegardgs-ksat added a commit to vegardgs-ksat/axum that referenced this pull request Oct 29, 2024
@vegardgs-ksat
Copy link
Author

* Is `ErrorKind::Message` still constructed anywhere? If not, would make sense to remove it, I wouldn't want to release this in a patch of 0.7 anyways.

It is indirectly constructed through the PathDeserializationError::custom() method, here and here

* Could you please add a changelog entry?

Done! Added it under the "# Unreleased" section

vegardgs-ksat added a commit to vegardgs-ksat/axum that referenced this pull request Oct 29, 2024
…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.
@vegardgs-ksat
Copy link
Author

I have a feeling that the failing CI job is unrelated to this PR.

@mladedav
Copy link
Collaborator

Yeah, it seems unrelated. I don't know the public api check but when I get some time, I'll try to figure out what's happening and then we'll merge this. Other branches seem to have also failed, it's possible one of our dependencies we're exposing started exposing another dependency we didn't mean to expose.

Sorry the whole thing takes so long.

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.

3 participants