ctoolbox/io/webui/
flexible_form.rs

1//! Axum extractor that can handle both `application/x-www-form-urlencoded` and
2//! `multipart/form-data` content types, using `axum::Form` and
3//! `axum_typed_multipart::TypedMultipart` internally.
4//!
5//! It will only work for forms that do not have file uploads, as those are
6//! represented as Bytes objects which can't be deserialized. For those, just
7//! use `TypedMultipart`.
8
9use axum::extract::{FromRequest, Request};
10use axum_extra::extract::{Form, FormRejection};
11use axum_typed_multipart::{
12    TryFromMultipartWithState, TypedMultipart, TypedMultipartError,
13};
14
15pub struct FlexibleForm<T>(pub T);
16
17/// Custom rejection for unsupported content types.
18#[derive(Debug)]
19pub struct UnsupportedContentTypeRejection {
20    pub error: String,
21}
22
23// Custom rejection type to handle both errors
24#[derive(Debug)]
25pub enum FlexibleFormRejection {
26    Multipart(TypedMultipartError),
27    Form(FormRejection),
28    UnsupportedContentType(UnsupportedContentTypeRejection),
29}
30
31impl<T, S> FromRequest<S> for FlexibleForm<T>
32where
33    T: TryFromMultipartWithState<S> + serde::de::DeserializeOwned,
34    S: Send + Sync,
35{
36    type Rejection = FlexibleFormRejection;
37
38    /// Attempts to extract the form data first as a Form, then as `TypedMultipart`.
39    async fn from_request(
40        req: Request,
41        state: &S,
42    ) -> Result<Self, Self::Rejection> {
43        let content_type = req
44            .headers()
45            .get(axum::http::header::CONTENT_TYPE)
46            .and_then(|v| v.to_str().ok())
47            .unwrap_or("");
48
49        match content_type {
50            "application/x-www-form-urlencoded" => {
51                // Try as form
52                match Form::<T>::from_request(req, state).await {
53                    Ok(Form(data)) => Ok(FlexibleForm(data)),
54                    Err(form_err) => Err(FlexibleFormRejection::Form(form_err)),
55                }
56            }
57            ct if ct.starts_with("multipart/form-data") => {
58                // Try as multipart
59                match TypedMultipart::<T>::from_request(req, state).await {
60                    Ok(TypedMultipart(data)) => Ok(FlexibleForm(data)),
61                    Err(multipart_err) => {
62                        Err(FlexibleFormRejection::Multipart(multipart_err))
63                    }
64                }
65            }
66            &_ => Err(FlexibleFormRejection::UnsupportedContentType(
67                UnsupportedContentTypeRejection {
68                    error: "Unsupported content type".into(),
69                },
70            )),
71        }
72    }
73}
74
75// Implement IntoResponse for the rejection if needed (for error handling)
76impl axum::response::IntoResponse for FlexibleFormRejection {
77    fn into_response(self) -> axum::response::Response {
78        match self {
79            FlexibleFormRejection::Multipart(e) => e.into_response(),
80            FlexibleFormRejection::Form(e) => e.into_response(),
81            FlexibleFormRejection::UnsupportedContentType(e) => {
82                use axum::http::StatusCode;
83                (StatusCode::UNSUPPORTED_MEDIA_TYPE, e.error).into_response()
84            }
85        }
86    }
87}