diff --git a/examples/parse-body-based-on-content-type/Cargo.toml b/examples/parse-body-based-on-content-type/Cargo.toml new file mode 100644 index 00000000..b3558db4 --- /dev/null +++ b/examples/parse-body-based-on-content-type/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-parse-body-based-on-content-type" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +axum = { path = "../../axum" } +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.0", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/examples/parse-body-based-on-content-type/src/main.rs b/examples/parse-body-based-on-content-type/src/main.rs new file mode 100644 index 00000000..9167d1e0 --- /dev/null +++ b/examples/parse-body-based-on-content-type/src/main.rs @@ -0,0 +1,89 @@ +//! Provides a RESTful web server managing some Todos. +//! +//! Run with +//! +//! ```not_rust +//! cd examples && cargo run -p example-parse-body-based-on-content-type +//! ``` + +use axum::{ + async_trait, + extract::FromRequest, + http::{header::CONTENT_TYPE, Request, StatusCode}, + response::{IntoResponse, Response}, + routing::post, + Form, Json, RequestExt, Router, +}; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() { + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new( + std::env::var("RUST_LOG").unwrap_or_else(|_| { + "example_parse_body_based_on_content_type=debug,tower_http=debug".into() + }), + )) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let app = Router::new().route("/", post(handler)); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + tracing::debug!("listening on {}", addr); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +#[derive(Serialize, Deserialize)] +struct Payload { + foo: String, +} + +async fn handler(payload: JsonOrForm) -> Response { + match payload { + JsonOrForm::Json(payload) => Json(payload).into_response(), + JsonOrForm::Form(payload) => Form(payload).into_response(), + } +} + +enum JsonOrForm { + Json(T), + Form(K), +} + +#[async_trait] +impl FromRequest for JsonOrForm +where + B: Send + 'static, + S: Send + Sync, + Json: FromRequest<(), B>, + Form: FromRequest<(), B>, + T: 'static, + U: 'static, +{ + type Rejection = Response; + + async fn from_request(req: Request, _state: &S) -> Result { + let content_type_header = req.headers().get(CONTENT_TYPE); + let content_type = content_type_header.and_then(|value| value.to_str().ok()); + + if let Some(content_type) = content_type { + if content_type.starts_with("application/json") { + let Json(payload) = req.extract().await.map_err(IntoResponse::into_response)?; + return Ok(Self::Json(payload)); + } + + if content_type.starts_with("application/x-www-form-urlencoded") { + let Form(payload) = req.extract().await.map_err(IntoResponse::into_response)?; + return Ok(Self::Form(payload)); + } + } + + Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()) + } +}