mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-26 00:00:23 +02:00
Only allow last extractor to mutate the request (#1272)
* Only allow last extractor to mutate the request * Change `FromRequest` and add `FromRequestParts` trait (#1275) * Add `Once`/`Mut` type parameter for `FromRequest` and `RequestParts` * 🪄 * split traits * `FromRequest` for tuples * Remove `BodyAlreadyExtracted` * don't need fully qualified path * don't export `Once` and `Mut` * remove temp tests * depend on axum again Co-authored-by: Jonas Platte <[email protected]> * Port `Handler` and most extractors (#1277) * Port `Handler` and most extractors * Put `M` inside `Handler` impls, not trait itself * comment out tuples for now * fix lints * Reorder arguments to `Handler` (#1281) I think `Request<B>, Arc<S>` is better since its consistent with `FromRequest` and `FromRequestParts`. * Port most things in axum-extra (#1282) * Port `#[derive(TypedPath)]` and `#[debug_handler]` (#1283) * port #[derive(TypedPath)] * wip: #[debug_handler] * fix #[debug_handler] * don't need itertools * also require `Send` * update expected error * support fully qualified `self` * Implement FromRequest[Parts] for tuples (#1286) * Port docs for axum and axum-core (#1285) * Port axum-extra (#1287) * Port axum-extra * Update axum-core/Cargo.toml Co-authored-by: Jonas Platte <[email protected]> * remove `impl FromRequest for Either*` Co-authored-by: Jonas Platte <[email protected]> * New FromRequest[Parts] trait cleanup (#1288) * Make private module truly private again * Simplify tuple FromRequest implementation * Port `#[derive(FromRequest)]` (#1289) * fix tests * fix docs * revert examples * fix docs link * fix intra docs links * Port examples (#1291) * Document wrapping other extractors (#1292) * axum-extra doesn't need to depend on axum-core (#1294) Missed this in https://github.com/tokio-rs/axum/pull/1287 * Add `FromRequest` changes to changelogs (#1293) * Update changelog * Remove default type for `S` in `Handler` * Clarify which types have default types for `S` * Apply suggestions from code review Co-authored-by: Jonas Platte <[email protected]> Co-authored-by: Jonas Platte <[email protected]> * remove unused import * Rename `Mut` and `Once` (#1296) * fix trybuild expected output Co-authored-by: Jonas Platte <[email protected]>
This commit is contained in:
co-authored by
Jonas Platte
parent
f1769e5134
commit
be624306f4
@@ -7,7 +7,7 @@
|
||||
use axum::{
|
||||
async_trait,
|
||||
body::{self, BoxBody, Bytes, Full},
|
||||
extract::{FromRequest, RequestParts},
|
||||
extract::FromRequest,
|
||||
http::{Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
@@ -72,31 +72,28 @@ fn do_thing_with_request_body(bytes: Bytes) {
|
||||
tracing::debug!(body = ?bytes);
|
||||
}
|
||||
|
||||
async fn handler(_: PrintRequestBody, body: Bytes) {
|
||||
async fn handler(BufferRequestBody(body): BufferRequestBody) {
|
||||
tracing::debug!(?body, "handler received body");
|
||||
}
|
||||
|
||||
// extractor that shows how to consume the request body upfront
|
||||
struct PrintRequestBody;
|
||||
struct BufferRequestBody(Bytes);
|
||||
|
||||
// we must implement `FromRequest` (and not `FromRequestParts`) to consume the body
|
||||
#[async_trait]
|
||||
impl<S> FromRequest<S, BoxBody> for PrintRequestBody
|
||||
impl<S> FromRequest<S, BoxBody> for BufferRequestBody
|
||||
where
|
||||
S: Clone + Send + Sync,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<S, BoxBody>) -> Result<Self, Self::Rejection> {
|
||||
let state = req.state().clone();
|
||||
|
||||
let request = Request::from_request(req)
|
||||
async fn from_request(req: Request<BoxBody>, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let body = Bytes::from_request(req, state)
|
||||
.await
|
||||
.map_err(|err| err.into_response())?;
|
||||
|
||||
let request = buffer_request_body(request).await?;
|
||||
do_thing_with_request_body(body.clone());
|
||||
|
||||
*req = RequestParts::with_state(state, request);
|
||||
|
||||
Ok(Self)
|
||||
Ok(Self(body))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
//! and `async/await`. This means that you can create more powerful rejections
|
||||
//! - Boilerplate: Requires creating a new extractor for every custom rejection
|
||||
//! - Complexity: Manually implementing `FromRequest` results on more complex code
|
||||
use axum::extract::MatchedPath;
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{rejection::JsonRejection, FromRequest, RequestParts},
|
||||
extract::{rejection::JsonRejection, FromRequest, FromRequestParts, MatchedPath},
|
||||
http::Request,
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
BoxError,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub async fn handler(Json(value): Json<Value>) -> impl IntoResponse {
|
||||
@@ -25,31 +23,33 @@ pub struct Json<T>(pub T);
|
||||
#[async_trait]
|
||||
impl<S, B, T> FromRequest<S, B> for Json<T>
|
||||
where
|
||||
axum::Json<T>: FromRequest<S, B, Rejection = JsonRejection>,
|
||||
S: Send + Sync,
|
||||
// these trait bounds are copied from `impl FromRequest for axum::Json`
|
||||
// `T: Send` is required to send this future across an await
|
||||
T: DeserializeOwned + Send,
|
||||
B: axum::body::HttpBody + Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<BoxError>,
|
||||
B: Send + 'static,
|
||||
{
|
||||
type Rejection = (StatusCode, axum::Json<Value>);
|
||||
|
||||
async fn from_request(req: &mut RequestParts<S, B>) -> Result<Self, Self::Rejection> {
|
||||
match axum::Json::<T>::from_request(req).await {
|
||||
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let (mut parts, body) = req.into_parts();
|
||||
|
||||
// We can use other extractors to provide better rejection
|
||||
// messages. For example, here we are using
|
||||
// `axum::extract::MatchedPath` to provide a better error
|
||||
// message
|
||||
//
|
||||
// Have to run that first since `Json::from_request` consumes
|
||||
// the request
|
||||
let path = MatchedPath::from_request_parts(&mut parts, state)
|
||||
.await
|
||||
.map(|path| path.as_str().to_owned())
|
||||
.ok();
|
||||
|
||||
let req = Request::from_parts(parts, body);
|
||||
|
||||
match axum::Json::<T>::from_request(req, state).await {
|
||||
Ok(value) => Ok(Self(value.0)),
|
||||
// convert the error from `axum::Json` into whatever we want
|
||||
Err(rejection) => {
|
||||
let path = req
|
||||
.extract::<MatchedPath>()
|
||||
.await
|
||||
.map(|x| x.as_str().to_owned())
|
||||
.ok();
|
||||
|
||||
// We can use other extractors to provide better rejection
|
||||
// messages. For example, here we are using
|
||||
// `axum::extract::MatchedPath` to provide a better error
|
||||
// message
|
||||
let payload = json!({
|
||||
"message": rejection.to_string(),
|
||||
"origin": "custom_extractor",
|
||||
|
||||
@@ -47,7 +47,7 @@ impl From<JsonRejection> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
// We implement `IntoResponse` so ApiError can be used as a response
|
||||
// We implement `IntoResponse` so `ApiError` can be used as a response
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let payload = json!({
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{path::ErrorKind, rejection::PathRejection, FromRequest, RequestParts},
|
||||
http::StatusCode,
|
||||
extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts},
|
||||
http::{request::Parts, StatusCode},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
@@ -52,17 +52,16 @@ struct Params {
|
||||
struct Path<T>(T);
|
||||
|
||||
#[async_trait]
|
||||
impl<S, B, T> FromRequest<S, B> for Path<T>
|
||||
impl<S, T> FromRequestParts<S> for Path<T>
|
||||
where
|
||||
// these trait bounds are copied from `impl FromRequest for axum::extract::path::Path`
|
||||
T: DeserializeOwned + Send,
|
||||
B: Send,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, axum::Json<PathError>);
|
||||
|
||||
async fn from_request(req: &mut RequestParts<S, B>) -> Result<Self, Self::Rejection> {
|
||||
match axum::extract::Path::<T>::from_request(req).await {
|
||||
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) => {
|
||||
let (status, body) = match rejection {
|
||||
|
||||
@@ -65,8 +65,8 @@ async fn users_show(
|
||||
|
||||
/// Handler for `POST /users`.
|
||||
async fn users_create(
|
||||
Json(params): Json<CreateUser>,
|
||||
State(user_repo): State<DynUserRepo>,
|
||||
Json(params): Json<CreateUser>,
|
||||
) -> Result<Json<User>, AppError> {
|
||||
let user = user_repo.create(params).await?;
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRequest, RequestParts, TypedHeader},
|
||||
extract::{FromRequestParts, TypedHeader},
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
http::StatusCode,
|
||||
http::{request::Parts, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
@@ -122,17 +122,16 @@ impl AuthBody {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S, B> FromRequest<S, B> for Claims
|
||||
impl<S> FromRequestParts<S> for Claims
|
||||
where
|
||||
S: Send + Sync,
|
||||
B: Send,
|
||||
{
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<S, B>) -> Result<Self, Self::Rejection> {
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
// Extract the token from the authorization header
|
||||
let TypedHeader(Authorization(bearer)) =
|
||||
TypedHeader::<Authorization<Bearer>>::from_request(req)
|
||||
TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|_| AuthError::InvalidToken)?;
|
||||
// Decode the user data
|
||||
|
||||
@@ -96,8 +96,8 @@ async fn kv_get(
|
||||
|
||||
async fn kv_set(
|
||||
Path(key): Path<String>,
|
||||
ContentLengthLimit(bytes): ContentLengthLimit<Bytes, { 1024 * 5_000 }>, // ~5mb
|
||||
State(state): State<SharedState>,
|
||||
ContentLengthLimit(bytes): ContentLengthLimit<Bytes, { 1024 * 5_000 }>, // ~5mb
|
||||
) {
|
||||
state.write().unwrap().db.insert(key, bytes);
|
||||
}
|
||||
|
||||
@@ -12,15 +12,14 @@ use async_session::{MemoryStore, Session, SessionStore};
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{
|
||||
rejection::TypedHeaderRejectionReason, FromRef, FromRequest, Query, RequestParts, State,
|
||||
TypedHeader,
|
||||
rejection::TypedHeaderRejectionReason, FromRef, FromRequestParts, Query, State, TypedHeader,
|
||||
},
|
||||
http::{header::SET_COOKIE, HeaderMap},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use http::header;
|
||||
use http::{header, request::Parts};
|
||||
use oauth2::{
|
||||
basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId,
|
||||
ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl,
|
||||
@@ -139,7 +138,7 @@ async fn discord_auth(State(client): State<BasicClient>) -> impl IntoResponse {
|
||||
.url();
|
||||
|
||||
// Redirect to Discord's oauth service
|
||||
Redirect::to(&auth_url.to_string())
|
||||
Redirect::to(auth_url.as_ref())
|
||||
}
|
||||
|
||||
// Valid user session required. If there is none, redirect to the auth page
|
||||
@@ -224,17 +223,18 @@ impl IntoResponse for AuthRedirect {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B> FromRequest<AppState, B> for User
|
||||
impl<S> FromRequestParts<S> for User
|
||||
where
|
||||
B: Send,
|
||||
MemoryStore: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
// If anything goes wrong or no session is found, redirect to the auth page
|
||||
type Rejection = AuthRedirect;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<AppState, B>) -> Result<Self, Self::Rejection> {
|
||||
let store = req.state().clone().store;
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let store = MemoryStore::from_ref(state);
|
||||
|
||||
let cookies = TypedHeader::<headers::Cookie>::from_request(req)
|
||||
let cookies = TypedHeader::<headers::Cookie>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|e| match *e.name() {
|
||||
header::COOKIE => match e.reason() {
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
use async_session::{MemoryStore, Session, SessionStore as _};
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRequest, RequestParts, TypedHeader},
|
||||
extract::{FromRef, FromRequestParts, TypedHeader},
|
||||
headers::Cookie,
|
||||
http::{
|
||||
self,
|
||||
header::{HeaderMap, HeaderValue},
|
||||
request::Parts,
|
||||
StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
@@ -80,16 +81,19 @@ enum UserIdFromSession {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B> FromRequest<MemoryStore, B> for UserIdFromSession
|
||||
impl<S> FromRequestParts<S> for UserIdFromSession
|
||||
where
|
||||
B: Send,
|
||||
MemoryStore: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request(req: &mut RequestParts<MemoryStore, B>) -> Result<Self, Self::Rejection> {
|
||||
let store = req.state().clone();
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let store = MemoryStore::from_ref(state);
|
||||
|
||||
let cookie = req.extract::<Option<TypedHeader<Cookie>>>().await.unwrap();
|
||||
let cookie = Option::<TypedHeader<Cookie>>::from_request_parts(parts, state)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session_cookie = cookie
|
||||
.as_ref()
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRequest, RequestParts, State},
|
||||
http::StatusCode,
|
||||
extract::{FromRef, FromRequestParts, State},
|
||||
http::{request::Parts, StatusCode},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
@@ -75,14 +75,15 @@ async fn using_connection_pool_extractor(
|
||||
struct DatabaseConnection(sqlx::pool::PoolConnection<sqlx::Postgres>);
|
||||
|
||||
#[async_trait]
|
||||
impl<B> FromRequest<PgPool, B> for DatabaseConnection
|
||||
impl<S> FromRequestParts<S> for DatabaseConnection
|
||||
where
|
||||
B: Send,
|
||||
PgPool: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, String);
|
||||
|
||||
async fn from_request(req: &mut RequestParts<PgPool, B>) -> Result<Self, Self::Rejection> {
|
||||
let pool = req.state().clone();
|
||||
async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let pool = PgPool::from_ref(state);
|
||||
|
||||
let conn = pool.acquire().await.map_err(internal_error)?;
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ struct CreateTodo {
|
||||
text: String,
|
||||
}
|
||||
|
||||
async fn todos_create(Json(input): Json<CreateTodo>, State(db): State<Db>) -> impl IntoResponse {
|
||||
async fn todos_create(State(db): State<Db>, Json(input): Json<CreateTodo>) -> impl IntoResponse {
|
||||
let todo = Todo {
|
||||
id: Uuid::new_v4(),
|
||||
text: input.text,
|
||||
@@ -125,8 +125,8 @@ struct UpdateTodo {
|
||||
|
||||
async fn todos_update(
|
||||
Path(id): Path<Uuid>,
|
||||
Json(input): Json<UpdateTodo>,
|
||||
State(db): State<Db>,
|
||||
Json(input): Json<UpdateTodo>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let mut todo = db
|
||||
.read()
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRequest, RequestParts, State},
|
||||
http::StatusCode,
|
||||
extract::{FromRef, FromRequestParts, State},
|
||||
http::{request::Parts, StatusCode},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
@@ -68,16 +68,15 @@ async fn using_connection_pool_extractor(
|
||||
struct DatabaseConnection(PooledConnection<'static, PostgresConnectionManager<NoTls>>);
|
||||
|
||||
#[async_trait]
|
||||
impl<B> FromRequest<ConnectionPool, B> for DatabaseConnection
|
||||
impl<S> FromRequestParts<S> for DatabaseConnection
|
||||
where
|
||||
B: Send,
|
||||
ConnectionPool: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, String);
|
||||
|
||||
async fn from_request(
|
||||
req: &mut RequestParts<ConnectionPool, B>,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let pool = req.state().clone();
|
||||
async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let pool = ConnectionPool::from_ref(state);
|
||||
|
||||
let conn = pool.get_owned().await.map_err(internal_error)?;
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::{Form, FromRequest, RequestParts},
|
||||
http::StatusCode,
|
||||
extract::{rejection::FormRejection, Form, FromRequest},
|
||||
http::{Request, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::get,
|
||||
BoxError, Router,
|
||||
Router,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use std::net::SocketAddr;
|
||||
@@ -64,14 +64,13 @@ impl<T, S, B> FromRequest<S, B> for ValidatedForm<T>
|
||||
where
|
||||
T: DeserializeOwned + Validate,
|
||||
S: Send + Sync,
|
||||
B: http_body::Body + Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<BoxError>,
|
||||
Form<T>: FromRequest<S, B, Rejection = FormRejection>,
|
||||
B: Send + 'static,
|
||||
{
|
||||
type Rejection = ServerError;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<S, B>) -> Result<Self, Self::Rejection> {
|
||||
let Form(value) = Form::<T>::from_request(req).await?;
|
||||
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(ValidatedForm(value))
|
||||
}
|
||||
@@ -83,7 +82,7 @@ pub enum ServerError {
|
||||
ValidationError(#[from] validator::ValidationErrors),
|
||||
|
||||
#[error(transparent)]
|
||||
AxumFormRejection(#[from] axum::extract::rejection::FormRejection),
|
||||
AxumFormRejection(#[from] FormRejection),
|
||||
}
|
||||
|
||||
impl IntoResponse for ServerError {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRequest, Path, RequestParts},
|
||||
http::StatusCode,
|
||||
extract::{FromRequestParts, Path},
|
||||
http::{request::Parts, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
@@ -48,15 +48,14 @@ enum Version {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S, B> FromRequest<S, B> for Version
|
||||
impl<S> FromRequestParts<S> for Version
|
||||
where
|
||||
B: Send,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<S, B>) -> Result<Self, Self::Rejection> {
|
||||
let params = Path::<HashMap<String, String>>::from_request(req)
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let params = Path::<HashMap<String, String>>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(IntoResponse::into_response)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user