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:
David Pedersen
2022-08-22 12:23:20 +02:00
committed by GitHub
co-authored by Jonas Platte
parent f1769e5134
commit be624306f4
104 changed files with 1513 additions and 1936 deletions
@@ -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?;
+5 -6
View File
@@ -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
+1 -1
View File
@@ -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);
}
+9 -9
View File
@@ -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() {
+10 -6
View File
@@ -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()
+7 -6
View File
@@ -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)?;
+2 -2
View File
@@ -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()
+7 -8
View File
@@ -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)?;
+8 -9
View File
@@ -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 {
+5 -6
View File
@@ -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)?;