HTTP request validation for Axum web framework

I'm trying to implement HTTP validation for Axum, just as we have in the official example, but I keep getting an error that says sync_trait proc_macro async_trait expected 1 argument, found 2. I really can't seem to figure out what I am doing wrong. Here is my implementation


/// use this to encapsulate fields that require validation
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidatedRequest<T>(pub T);

#[async_trait]
impl<T, S, B> FromRequest<S, B> for ValidatedRequest<T>
where
    T: DeserializeOwned + Validate,
    S: Send + Sync,
    Form<T>: FromRequest<S, B, Rejection = FormRejection>,
    B: Send + 'static,
{
    type Rejection = ApiErrorResponse;

    async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
        let Form(value) = Form::<T>::from_request(req, state).await?;
        value.validate()?;
        Ok(ValidatedRequest(value))
    }
}

You're probably using axum 0.5, where from_request only has one parameter: FromRequest in axum::extract - Rust

The example you linked is from the main branch and includes the axum dependency (here) like this: axum = { path = "../../axum" }. In other words it's using the absolute latest in-development version of axum and it's not guaranteed to work with any version of axum that's on crates.io. You'll want to look at the tags and find one that's associated with the version you're using, for example axum/main.rs at axum-v0.5.16 · tokio-rs/axum · GitHub is the example at the axum-v0.5.16 tag.

2 Likes

Thank you that works.
However, I think I ran into a bigger issue.

``` the official example uses accept form data. On the contrary, my API is supposed to accept JSON. Is there any modification that can be made to achieve this. I tried changing Form to Json, but it won work

let Json(value) = Json::<T>::from_request(req).await?;

Here is the full code

/// use this to encapsulate fields that require validation
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidatedRequest<T>(pub T);

#[async_trait]
impl<T, B> FromRequest<B> for ValidatedRequest<T>
where
    T: DeserializeOwned + Validate,
    B: http_body::Body + Send,
    B::Data: Send,
    B::Error: Into<BoxError>,
{
    type Rejection = ServerError;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let Form(value) = Form::<T>::from_request(req).await?;
        value.validate()?;
        Ok(ValidatedRequest(value))
    }
}

The Json extractor builds fine for me

/// use this to encapsulate fields that require validation
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidatedRequest<T>(pub T);

#[async_trait]
impl<T, B> FromRequest<B> for ValidatedRequest<T>
where
    T: DeserializeOwned + Validate,
    B: HttpBody + Send,
    B::Data: Send,
    B::Error: Into<BoxError>,
{
    type Rejection = ServerError;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req).await?;
        value.validate()?;
        Ok(ValidatedRequest(value))
    }
}

Did you enable the json feature on axum? If you did, can you include the actual error you're seeing?

2 Likes

I'm still getting the same error even with your solution

 `?` couldn't convert the error to `ServerError`
   --> src/shared/api_response.rs:229:61
    |
229 |         let Json(value) = Json::<T>::from_request(req).await?;
    |                                                             ^ the trait `std::convert::From<JsonRejection>` is not implemented for `ServerError`
    |```

I figured out what the error is but, I'm still stuck providing a solution.
here is the solution I'm put forth

impl std::convert::From<JsonRejection> for ServerError {
    fn from(error: JsonRejection) -> Self {
        println!("{:#?}", error);
        // Self::ValidationError(error)
        todo!()
    }
}

I'm not sure what goes into the todo!() body, here is the full code


/// use this to encapsulate fields that require validation
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidatedRequest<T>(pub T);

#[async_trait]
impl<T, B> FromRequest<B> for ValidatedRequest<T>
where
    T: DeserializeOwned + Validate,
    B: http_body::Body + Send,
    B::Data: Send,
    B::Error: Into<BoxError>,
{
    type Rejection = ServerError;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req).await?;
        value.validate()?;
        Ok(ValidatedRequest(value))
    }
}
#[derive(Debug, Error)]
pub enum ServerError {
    #[error(transparent)]
    ValidationError(#[from] validator::ValidationErrors),

    #[error(transparent)]
    AxumFormRejection(#[from] axum::extract::rejection::FormRejection),
}

impl IntoResponse for ServerError {
    fn into_response(self) -> Response {
        match self {
            ServerError::ValidationError(_) => {
                let message = format!("Input validation error: [{}]", self).replace('\n', ", ");
                (StatusCode::BAD_REQUEST, message)
            }
            ServerError::AxumFormRejection(_) => (StatusCode::BAD_REQUEST, self.to_string()),
        }
        .into_response()
    }
}

impl std::convert::From<JsonRejection> for ServerError {
    fn from(error: JsonRejection) -> Self {
        println!("{:#?}", error);
        // Self::ValidationError(error)
        todo!()
    }
}

Here is the full code

Just change the FormRejection in ServerError to the JsonRejection

#[derive(Debug, Error)]
pub enum ServerError {
    #[error(transparent)]
    ValidationError(#[from] validator::ValidationErrors),

    #[error(transparent)]
    AxumJsonRejection(#[from] axum::extract::rejection::JsonRejection),
}

You'll have to update the name in a couple places too obviously.