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

Implement OptionalFromRequest for the Json extractor #3142

Merged
merged 6 commits into from
Jan 3, 2025

Conversation

infiniteregrets
Copy link
Contributor

Motivation

I want to receive requests with an empty body when using the Json extractor without the request getting rejected. I saw that OptionalFromRequest has been implemented for Query and Path and I think this is quite useful to have as it allows me to do something like:

pub async fn create(
    State(account_service): State<Arc<AccountService>>,
    Path(name): Path<String>,
    WithRejection(payload, _): WithRejection<Option<Json<CreateRequest>>, ServiceError>,
) -> Result<impl IntoResponse, ServiceError> {
...

    let Json(payload) = payload.unwrap_or_default();
...
}

Solution

Implement OptionalFromRequest for the Json extractor.

I have tested that this works locally for myself. I don't know if this is absolutely the right way/per standards of how something like this should be approached for axum. However very quick change, so I thought I'd open a PR. Please correct me if I am wrong anywhere, I'll be happy to make any changes/close the PR if not in plans!

Copy link
Collaborator

@Turbo87 Turbo87 left a comment

Choose a reason for hiding this comment

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

given the various ways in which None could be interpreted, I'm wondering whether this causes more confusion than it solves. it might be better to use Result<Json, _> as the extractor and explicitly handle the corresponding error cases 🤔

axum/src/json.rs Outdated
if !bytes.is_empty() {
Ok(Some(Self::from_bytes(&bytes)?))
} else {
Ok(None)
Copy link
Collaborator

Choose a reason for hiding this comment

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

if the header says that the content-type is JSON, but the body is empty, shouldn't that be an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah that makes sense, I added a new error, is that the right way to approach this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

what rejection is the non-optional extractor using in this case? I assume JsonSyntaxError? would it be possible to reuse that rejection?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I looked into that one, I am not quite sure what Error is supposed to be here in JsonSyntaxError(Error) ? Is it really just specific to serde?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess we should always call Self::from_bytes() even if bytes.is_empty(), because if the header was set then we expect valid JSON and an empty body is not valid JSON 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

do i need to create a deserializer and then use serde_path_to_error::deserialize to use that error variant?

Copy link
Contributor Author

@infiniteregrets infiniteregrets Jan 3, 2025

Choose a reason for hiding this comment

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

I guess we should always call Self::from_bytes() even if bytes.is_empty(), because if the header was set then we expect valid JSON and an empty body is not valid JSON 🤔

do you mean something like this right in the block where we check if the body is empty:?

match Self::from_bytes(&bytes) {
  Ok(_) => unreachable!("JSON body can only be empty here"),
  Err(err) => Err(err),
}               

if json_content_type(req.headers()) {
let bytes = Bytes::from_request(req, state).await?;
if !bytes.is_empty() {
Ok(Some(Self::from_bytes(&bytes)?))
Copy link
Collaborator

Choose a reason for hiding this comment

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

this causes a mismatching JSON body to return an error instead of None, is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is this in reference to the previous CR comment? by mismatch do you mean an empty body here?

@jplatte
Copy link
Member

jplatte commented Jan 3, 2025

I like it but I think only a request with no content-type header and an empty body should result in None. Would that fit your use case, @infiniteregrets?

@infiniteregrets
Copy link
Contributor Author

thanks for the review!

@jplatte that does fit my use case here!

axum/src/json.rs Outdated Show resolved Hide resolved
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, this looks good to me. Thanks!

@jplatte
Copy link
Member

jplatte commented Jan 3, 2025

Could you add a changelog entry for this?

@infiniteregrets
Copy link
Contributor Author

yup!

axum/CHANGELOG.md Outdated Show resolved Hide resolved
@jplatte jplatte enabled auto-merge (squash) January 3, 2025 19:16
@jplatte jplatte merged commit ef0b99b into tokio-rs:main Jan 3, 2025
18 checks passed
@Turbo87
Copy link
Collaborator

Turbo87 commented Jan 4, 2025

I guess the new behavior should also be documented in the doc comment?

@jplatte
Copy link
Member

jplatte commented Jan 4, 2025

Ah, yeah.

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