Merge remote-tracking branch 'origin/main' into david/dont-override-status-codes-of-5xx

This commit is contained in:
Yann Simon 2024-11-30 15:10:01 +01:00
commit cb760ba45b
No known key found for this signature in database
330 changed files with 6760 additions and 2682 deletions

View file

@ -2,7 +2,7 @@ name: CI
env:
CARGO_TERM_COLOR: always
MSRV: '1.66'
MSRV: '1.75'
on:
push:
@ -12,37 +12,46 @@ on:
jobs:
check:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: taiki-e/install-action@protoc
- uses: dtolnay/rust-toolchain@beta
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Check
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: rustfmt
run: cargo fmt --all --check
check-docs:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: cargo doc
env:
RUSTDOCFLAGS: "-D rustdoc::all -A rustdoc::private-doc-tests"
run: cargo doc --all-features --no-deps
cargo-hack:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: taiki-e/install-action@protoc
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Install cargo-hack
run: |
curl -LsSf https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin
@ -50,42 +59,54 @@ jobs:
run: cargo hack check --each-feature --no-dev-deps --all
cargo-public-api-crates:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
strategy:
matrix:
crate: [axum, axum-core, axum-extra, axum-macros]
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly
- uses: actions/checkout@v4
# Pinned version due to failing `cargo-public-api-crates`.
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-06
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Install cargo-public-api-crates
run: |
cargo install --git https://github.com/davidpdrsn/cargo-public-api-crates
- name: Build rustdoc
run: |
cargo rustdoc --all-features --manifest-path ${{ matrix.crate }}/Cargo.toml -- -Z unstable-options --output-format json
- name: cargo public-api-crates check
run: cargo public-api-crates --manifest-path ${{ matrix.crate }}/Cargo.toml check
run: cargo public-api-crates --manifest-path ${{ matrix.crate }}/Cargo.toml --skip-build check
test-versions:
needs: check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
strategy:
matrix:
rust: [stable, beta]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: taiki-e/install-action@protoc
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Run tests
run: cargo test --workspace --all-features --all-targets
# some examples doesn't support our MSRV so we only test axum itself on our MSRV
# some examples don't support our MSRV so we only test axum itself on our MSRV
test-nightly:
needs: check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get rust-toolchain version
id: rust-toolchain
run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT
@ -93,23 +114,29 @@ jobs:
with:
toolchain: ${{ steps.rust-toolchain.outputs.version }}
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Run nightly tests
working-directory: axum-macros
run: cargo test
# some examples doesn't support our MSRV (such as async-graphql)
# some examples don't support our MSRV (such as async-graphql)
# so we only test axum itself on our MSRV
test-msrv:
needs: check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.MSRV }}
- name: "install Rust nightly"
uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Select minimal version
run: cargo +nightly update -Z minimal-versions
- name: Fix up Cargo.lock
@ -137,17 +164,20 @@ jobs:
test-docs:
needs: check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Run doc tests
run: cargo test --all-features --doc
deny-check:
name: cargo-deny check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
continue-on-error: ${{ matrix.checks == 'advisories' }}
strategy:
matrix:
@ -155,21 +185,24 @@ jobs:
- advisories
- bans licenses sources
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
with:
command: check ${{ matrix.checks }}
arguments: --all-features --manifest-path axum/Cargo.toml
manifest-path: axum/Cargo.toml
armv5te-unknown-linux-musleabi:
needs: check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
target: armv5te-unknown-linux-musleabi
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Check
env:
# Clang has native cross-compilation support
@ -187,13 +220,16 @@ jobs:
wasm32-unknown-unknown:
needs: check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Check
run: >
cargo
@ -202,11 +238,14 @@ jobs:
--target wasm32-unknown-unknown
dependencies-are-sorted:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@beta
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Install cargo-sort
run: |
cargo install cargo-sort
@ -219,12 +258,12 @@ jobs:
typos:
name: Spell Check with Typos
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.16.2
uses: crate-ci/typos@v1.20.8

View file

@ -267,7 +267,7 @@ If a Pull Request appears to be abandoned or stalled, it is polite to first
check with the contributor to see if they intend to continue the work before
checking if they would mind if you took it over (especially if it just has nits
left). When doing so, it is courteous to give the original contributor credit
for the work they started (either by preserving their name and email address in
for the work they started, either by preserving their name and email address in
the commit log, or by using an `Author: ` meta-data tag in the commit.
[hiding-a-comment]: https://help.github.com/articles/managing-disruptive-comments/#hiding-a-comment

View file

@ -5,3 +5,6 @@ default-members = ["axum", "axum-*"]
# Example has been deleted, but README.md remains
exclude = ["examples/async-graphql"]
resolver = "2"
[workspace.package]
rust-version = "1.75"

View file

@ -26,8 +26,8 @@ If your project isn't listed here and you would like it to be, please feel free
- [axum-guard-logic](https://github.com/sjud/axum_guard_logic): Use AND/OR logic to extract types and check their values against `Service` inputs.
- [axum-casbin-auth](https://github.com/casbin-rs/axum-casbin-auth): Casbin access control middleware for axum framework
- [aide](https://docs.rs/aide): Code-first Open API documentation generator with [axum integration](https://docs.rs/aide/latest/aide/axum/index.html).
- [axum-typed-routing](https://docs.rs/axum-typed-routing/latest/axum_typed_routing/): Statically typed routing macros with OpenAPI generation using aide.
- [axum-jsonschema](https://docs.rs/axum-jsonschema/): A `Json<T>` extractor that does JSON schema validation of requests.
- [axum-sessions](https://docs.rs/axum-sessions): Cookie-based sessions for axum via async-session.
- [axum-login](https://docs.rs/axum-login): Session-based user authentication for axum.
- [axum-csrf-sync-pattern](https://crates.io/crates/axum-csrf-sync-pattern): A middleware implementing CSRF STP for AJAX backends and API endpoints.
- [axum-otel-metrics](https://github.com/ttys3/axum-otel-metrics/): A axum OpenTelemetry Metrics middleware with prometheus exporter supported.
@ -45,6 +45,11 @@ If your project isn't listed here and you would like it to be, please feel free
- [socketioxide](https://github.com/totodore/socketioxide): An easy to use socket.io server implementation working as a `tower` layer/service.
- [axum-serde](https://github.com/gengteng/axum-serde): Provides multiple serde-based extractors / responses, also offers a macro to easily customize serde-based extractors / responses.
- [loco.rs](https://github.com/loco-rs/loco): A full stack Web and API productivity framework similar to Rails, based on Axum.
- [axum-test](https://crates.io/crates/axum-test): High level library for writing Cargo tests that run against Axum.
- [axum-messages](https://github.com/maxcountryman/axum-messages): One-time notification messages for Axum.
- [spring-rs](https://github.com/spring-rs/spring-rs): spring-rs is a microservice framework written in rust inspired by java's spring-boot, based on axum
- [zino](https://github.com/zino-rs/zino): Zino is a next-generation framework for composable applications which provides full integrations with axum.
- [axum-rails-cookie](https://github.com/endoze/axum-rails-cookie): Extract rails session cookies in axum based apps.
## Project showcase
@ -58,6 +63,8 @@ If your project isn't listed here and you would like it to be, please feel free
- [realworld-axum-sqlx](https://github.com/launchbadge/realworld-axum-sqlx): A Rust implementation of the [Realworld] demo app spec using Axum and [SQLx].
See https://github.com/davidpdrsn/realworld-axum-sqlx for a fork with up to date dependencies.
- [Rustapi](https://github.com/ndelvalle/rustapi): RESTful API template using MongoDB
- [axum-postgres-template](https://github.com/koskeller/axum-postgres-template): Production-ready Axum + PostgreSQL application template
- [RUSTfulapi](https://github.com/robatipoor/rustfulapi): Reusable template for building REST Web Services in Rust. Uses Axum HTTP web framework and SeaORM.
- [Jotsy](https://github.com/ohsayan/jotsy): Self-hosted notes app powered by Skytable, Axum and Tokio
- [Svix](https://www.svix.com) ([repository](https://github.com/svix/svix-webhooks)): Enterprise-ready webhook service
- [emojied](https://emojied.net) ([repository](https://github.com/sekunho/emojied)): Shorten URLs to emojis!
@ -78,10 +85,12 @@ If your project isn't listed here and you would like it to be, please feel free
- [cobrust](https://github.com/scotow/cobrust): Multiplayer web based snake game.
- [meta-cross](https://github.com/scotow/meta-cross): Tweaked version of Tic-Tac-Toe.
- [httq](https://github.com/scotow/httq) HTTP to MQTT trivial proxy.
- [Pods-Blitz](https://pods-blitz.org) Self-hosted podcast publisher. Uses the crates axum-login, password-auth, sqlx and handlebars (for HTML templates).
- [ReductStore](https://github.com/reductstore/reductstore): A time series database for storing and managing large amounts of blob data
- [randoku](https://github.com/stchris/randoku): A tiny web service which generates random numbers and shuffles lists randomly
- [sero](https://github.com/clowzed/sero): Host static sites with custom subdomains as surge.sh does. But with full control and cool new features. (axum, sea-orm, postgresql)
- [Hatsu](https://github.com/importantimport/hatsu): 🩵 Self-hosted & Fully-automated ActivityPub Bridge for Static Sites.
- [Mini RPS](https://github.com/marcodpt/minirps): Mini reverse proxy server, HTTPS, CORS, static file hosting and template engine (minijinja).
[Realworld]: https://github.com/gothinkster/realworld
[SQLx]: https://github.com/launchbadge/sqlx
@ -97,6 +106,7 @@ If your project isn't listed here and you would like it to be, please feel free
- [Introduction to axum]: YouTube playlist
- [Rust Axum Full Course]: YouTube video
- [Deploying Axum projects with Shuttle]
- [API Development with Rust](https://rust-api.dev/docs/front-matter/preface/): REST APIs based on Axum
[axum-tutorial]: https://github.com/programatik29/axum-tutorial
[axum-tutorial-website]: https://programatik29.github.io/axum-tutorial/

View file

@ -5,9 +5,44 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
# 0.5.0
- None.
## alpha.1
- **breaking:** Replace `#[async_trait]` with [return-position `impl Trait` in traits][RPITIT] ([#2308])
- **change:** Update minimum rust version to 1.75 ([#2943])
[RPITIT]: https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html
[#2308]: https://github.com/tokio-rs/axum/pull/2308
[#2943]: https://github.com/tokio-rs/axum/pull/2943
# 0.4.5
- **fixed:** Compile errors from the internal `__log_rejection` macro under
certain Cargo feature combinations between axum crates ([#2933])
[#2933]: https://github.com/tokio-rs/axum/pull/2933
# 0.4.4
- **added:** Derive `Clone` and `Copy` for `AppendHeaders` ([#2776])
- **added:** `must_use` attribute on `AppendHeaders` ([#2846])
- **added:** `must_use` attribute on `ErrorResponse` ([#2846])
- **added:** `must_use` attribute on `IntoResponse::into_response` ([#2846])
- **added:** `must_use` attribute on `IntoResponseParts` trait methods ([#2846])
- **added:** Implement `Copy` for `DefaultBodyLimit` ([#2875])
- **added**: `DefaultBodyLimit::max` and `DefaultBodyLimit::disable` are now
allowed in const context ([#2875])
[#2776]: https://github.com/tokio-rs/axum/pull/2776
[#2846]: https://github.com/tokio-rs/axum/pull/2846
[#2875]: https://github.com/tokio-rs/axum/pull/2875
# 0.4.3 (13. January, 2024)
- **added:** Implement `IntoResponseParts` for `()` ([#2471])
[#2471]: https://github.com/tokio-rs/axum/pull/2471
# 0.4.2 (29. December, 2023)

View file

@ -2,14 +2,14 @@
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Core types and traits for axum"
edition = "2021"
rust-version = "1.56"
rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
license = "MIT"
name = "axum-core"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.4.2" # remember to also bump the version that axum and axum-extra depend on
version = "0.5.0-alpha.1" # remember to bump the version that axum and axum-extra depend on
[features]
tracing = ["dep:tracing"]
@ -18,32 +18,29 @@ tracing = ["dep:tracing"]
__private_docs = ["dep:tower-http"]
[dependencies]
async-trait = "0.1.67"
bytes = "1.0"
bytes = "1.2"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0"
http-body = "1.0.0"
http-body-util = "0.1.0"
mime = "0.3.16"
pin-project-lite = "0.2.7"
sync_wrapper = "0.1.1"
rustversion = "1.0.9"
sync_wrapper = "1.0.0"
tower-layer = "0.3"
tower-service = "0.3"
# optional dependencies
tower-http = { version = "0.5.0", optional = true, features = ["limit"] }
tower-http = { version = "0.6.0", optional = true, features = ["limit"] }
tracing = { version = "0.1.37", default-features = false, optional = true }
[build-dependencies]
rustversion = "1.0.9"
[dev-dependencies]
axum = { path = "../axum", version = "0.7.2" }
axum = { path = "../axum" }
axum-extra = { path = "../axum-extra", features = ["typed-header"] }
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
hyper = "1.0.0"
tokio = { version = "1.25.0", features = ["macros"] }
tower-http = { version = "0.5.0", features = ["limit"] }
tower-http = { version = "0.6.0", features = ["limit"] }
[package.metadata.cargo-public-api-crates]
allowed = [
@ -57,6 +54,8 @@ allowed = [
"http_body",
]
[package.metadata.cargo-machete]
ignored = ["tower-http"] # See __private_docs feature
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View file

@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum-core's MSRV is 1.56.
axum-core's MSRV is 1.75.
## Getting Help

View file

@ -1,7 +0,0 @@
#[rustversion::nightly]
fn main() {
println!("cargo:rustc-cfg=nightly_error_messages");
}
#[rustversion::not(nightly)]
fn main() {}

View file

@ -6,13 +6,11 @@ mod tests {
use std::convert::Infallible;
use crate::extract::{FromRef, FromRequestParts};
use async_trait::async_trait;
use http::request::Parts;
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct State<S>(pub(crate) S);
#[async_trait]
impl<OuterState, InnerState> FromRequestParts<OuterState> for State<InnerState>
where
InnerState: FromRef<OuterState>,
@ -30,9 +28,9 @@ mod tests {
}
// some extractor that requires the state, such as `SignedCookieJar`
#[allow(dead_code)]
pub(crate) struct RequiresState(pub(crate) String);
#[async_trait]
impl<S> FromRequestParts<S> for RequiresState
where
S: Send + Sync,

View file

@ -1,6 +1,6 @@
use crate::body::Body;
use crate::extract::{DefaultBodyLimitKind, FromRequest, FromRequestParts, Request};
use futures_util::future::BoxFuture;
use std::future::Future;
mod sealed {
pub trait Sealed {}
@ -20,7 +20,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
///
/// ```
/// use axum::{
/// async_trait,
/// extract::{Request, FromRequest},
/// body::Body,
/// http::{header::CONTENT_TYPE, StatusCode},
@ -30,7 +29,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
///
/// struct FormOrJson<T>(T);
///
/// #[async_trait]
/// impl<S, T> FromRequest<S> for FormOrJson<T>
/// where
/// Json<T>: FromRequest<()>,
@ -67,7 +65,7 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// }
/// }
/// ```
fn extract<E, M>(self) -> BoxFuture<'static, Result<E, E::Rejection>>
fn extract<E, M>(self) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequest<(), M> + 'static,
M: 'static;
@ -83,7 +81,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
///
/// ```
/// use axum::{
/// async_trait,
/// body::Body,
/// extract::{Request, FromRef, FromRequest},
/// RequestExt,
@ -93,7 +90,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// requires_state: RequiresState,
/// }
///
/// #[async_trait]
/// impl<S> FromRequest<S> for MyExtractor
/// where
/// String: FromRef<S>,
@ -111,7 +107,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// // some extractor that consumes the request body and requires state
/// struct RequiresState { /* ... */ }
///
/// #[async_trait]
/// impl<S> FromRequest<S> for RequiresState
/// where
/// String: FromRef<S>,
@ -124,7 +119,10 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// # }
/// }
/// ```
fn extract_with_state<E, S, M>(self, state: &S) -> BoxFuture<'_, Result<E, E::Rejection>>
fn extract_with_state<E, S, M>(
self,
state: &S,
) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequest<S, M> + 'static,
S: Send + Sync;
@ -137,7 +135,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
///
/// ```
/// use axum::{
/// async_trait,
/// extract::{Path, Request, FromRequest},
/// response::{IntoResponse, Response},
/// body::Body,
@ -154,7 +151,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// payload: T,
/// }
///
/// #[async_trait]
/// impl<S, T> FromRequest<S> for MyExtractor<T>
/// where
/// S: Send + Sync,
@ -179,7 +175,7 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// }
/// }
/// ```
fn extract_parts<E>(&mut self) -> BoxFuture<'_, Result<E, E::Rejection>>
fn extract_parts<E>(&mut self) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequestParts<()> + 'static;
@ -191,7 +187,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
///
/// ```
/// use axum::{
/// async_trait,
/// extract::{Request, FromRef, FromRequest, FromRequestParts},
/// http::request::Parts,
/// response::{IntoResponse, Response},
@ -204,7 +199,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// payload: T,
/// }
///
/// #[async_trait]
/// impl<S, T> FromRequest<S> for MyExtractor<T>
/// where
/// String: FromRef<S>,
@ -234,7 +228,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
///
/// struct RequiresState {}
///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for RequiresState
/// where
/// String: FromRef<S>,
@ -250,7 +243,7 @@ pub trait RequestExt: sealed::Sealed + Sized {
fn extract_parts_with_state<'a, E, S>(
&'a mut self,
state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>>
) -> impl Future<Output = Result<E, E::Rejection>> + Send + 'a
where
E: FromRequestParts<S> + 'static,
S: Send + Sync;
@ -267,7 +260,7 @@ pub trait RequestExt: sealed::Sealed + Sized {
}
impl RequestExt for Request {
fn extract<E, M>(self) -> BoxFuture<'static, Result<E, E::Rejection>>
fn extract<E, M>(self) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequest<(), M> + 'static,
M: 'static,
@ -275,7 +268,10 @@ impl RequestExt for Request {
self.extract_with_state(&())
}
fn extract_with_state<E, S, M>(self, state: &S) -> BoxFuture<'_, Result<E, E::Rejection>>
fn extract_with_state<E, S, M>(
self,
state: &S,
) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequest<S, M> + 'static,
S: Send + Sync,
@ -283,17 +279,17 @@ impl RequestExt for Request {
E::from_request(self, state)
}
fn extract_parts<E>(&mut self) -> BoxFuture<'_, Result<E, E::Rejection>>
fn extract_parts<E>(&mut self) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequestParts<()> + 'static,
{
self.extract_parts_with_state(&())
}
fn extract_parts_with_state<'a, E, S>(
async fn extract_parts_with_state<'a, E, S>(
&'a mut self,
state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>>
) -> Result<E, E::Rejection>
where
E: FromRequestParts<S> + 'static,
S: Send + Sync,
@ -306,17 +302,15 @@ impl RequestExt for Request {
*req.extensions_mut() = std::mem::take(self.extensions_mut());
let (mut parts, ()) = req.into_parts();
Box::pin(async move {
let result = E::from_request_parts(&mut parts, state).await;
let result = E::from_request_parts(&mut parts, state).await;
*self.version_mut() = parts.version;
*self.method_mut() = parts.method.clone();
*self.uri_mut() = parts.uri.clone();
*self.headers_mut() = std::mem::take(&mut parts.headers);
*self.extensions_mut() = std::mem::take(&mut parts.extensions);
*self.version_mut() = parts.version;
*self.method_mut() = parts.method.clone();
*self.uri_mut() = parts.uri.clone();
*self.headers_mut() = std::mem::take(&mut parts.headers);
*self.extensions_mut() = std::mem::take(&mut parts.extensions);
result
})
result
}
fn with_limited_body(self) -> Request {
@ -345,7 +339,6 @@ mod tests {
ext_traits::tests::{RequiresState, State},
extract::FromRef,
};
use async_trait::async_trait;
use http::Method;
#[tokio::test]
@ -414,7 +407,6 @@ mod tests {
body: String,
}
#[async_trait]
impl<S> FromRequest<S> for WorksForCustomExtractor
where
S: Send + Sync,

View file

@ -1,6 +1,6 @@
use crate::extract::FromRequestParts;
use futures_util::future::BoxFuture;
use http::request::Parts;
use std::future::Future;
mod sealed {
pub trait Sealed {}
@ -21,7 +21,6 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// response::{Response, IntoResponse},
/// http::request::Parts,
/// RequestPartsExt,
/// async_trait,
/// };
/// use std::collections::HashMap;
///
@ -30,7 +29,6 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// query_params: HashMap<String, String>,
/// }
///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for MyExtractor
/// where
/// S: Send + Sync,
@ -54,7 +52,7 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// }
/// }
/// ```
fn extract<E>(&mut self) -> BoxFuture<'_, Result<E, E::Rejection>>
fn extract<E>(&mut self) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequestParts<()> + 'static;
@ -70,14 +68,12 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// response::{Response, IntoResponse},
/// http::request::Parts,
/// RequestPartsExt,
/// async_trait,
/// };
///
/// struct MyExtractor {
/// requires_state: RequiresState,
/// }
///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for MyExtractor
/// where
/// String: FromRef<S>,
@ -97,7 +93,6 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// struct RequiresState { /* ... */ }
///
/// // some extractor that requires a `String` in the state
/// #[async_trait]
/// impl<S> FromRequestParts<S> for RequiresState
/// where
/// String: FromRef<S>,
@ -113,14 +108,14 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
fn extract_with_state<'a, E, S>(
&'a mut self,
state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>>
) -> impl Future<Output = Result<E, E::Rejection>> + Send + 'a
where
E: FromRequestParts<S> + 'static,
S: Send + Sync;
}
impl RequestPartsExt for Parts {
fn extract<E>(&mut self) -> BoxFuture<'_, Result<E, E::Rejection>>
fn extract<E>(&mut self) -> impl Future<Output = Result<E, E::Rejection>> + Send
where
E: FromRequestParts<()> + 'static,
{
@ -130,7 +125,7 @@ impl RequestPartsExt for Parts {
fn extract_with_state<'a, E, S>(
&'a mut self,
state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>>
) -> impl Future<Output = Result<E, E::Rejection>> + Send + 'a
where
E: FromRequestParts<S> + 'static,
S: Send + Sync,
@ -148,7 +143,6 @@ mod tests {
ext_traits::tests::{RequiresState, State},
extract::FromRef,
};
use async_trait::async_trait;
use http::{Method, Request};
#[tokio::test]
@ -181,7 +175,6 @@ mod tests {
from_state: String,
}
#[async_trait]
impl<S> FromRequestParts<S> for WorksForCustomExtractor
where
S: Send + Sync,

View file

@ -72,7 +72,7 @@ use tower_layer::Layer;
/// [`RequestBodyLimit`]: tower_http::limit::RequestBodyLimit
/// [`RequestExt::with_limited_body`]: crate::RequestExt::with_limited_body
/// [`RequestExt::into_limited_body`]: crate::RequestExt::into_limited_body
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct DefaultBodyLimit {
kind: DefaultBodyLimitKind,
@ -116,7 +116,7 @@ impl DefaultBodyLimit {
/// [`Bytes`]: bytes::Bytes
/// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html
/// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.html
pub fn disable() -> Self {
pub const fn disable() -> Self {
Self {
kind: DefaultBodyLimitKind::Disable,
}
@ -149,7 +149,7 @@ impl DefaultBodyLimit {
/// [`Bytes::from_request`]: bytes::Bytes
/// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html
/// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.html
pub fn max(limit: usize) -> Self {
pub const fn max(limit: usize) -> Self {
Self {
kind: DefaultBodyLimitKind::Limit(limit),
}

View file

@ -5,9 +5,9 @@
//! [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
use crate::{body::Body, response::IntoResponse};
use async_trait::async_trait;
use http::request::Parts;
use std::convert::Infallible;
use std::future::Future;
pub mod rejection;
@ -42,9 +42,8 @@ mod private {
/// See [`axum::extract`] for more general docs about extractors.
///
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
#[async_trait]
#[cfg_attr(
nightly_error_messages,
#[rustversion::attr(
since(1.78),
diagnostic::on_unimplemented(
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/0.7/axum/extract/index.html` for details",
)
@ -55,7 +54,10 @@ pub trait FromRequestParts<S>: Sized {
type Rejection: IntoResponse;
/// Perform the extraction.
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection>;
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}
/// Types that can be created from requests.
@ -69,9 +71,8 @@ pub trait FromRequestParts<S>: Sized {
/// See [`axum::extract`] for more general docs about extractors.
///
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
#[async_trait]
#[cfg_attr(
nightly_error_messages,
#[rustversion::attr(
since(1.78),
diagnostic::on_unimplemented(
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/0.7/axum/extract/index.html` for details",
)
@ -82,10 +83,12 @@ pub trait FromRequest<S, M = private::ViaRequest>: Sized {
type Rejection: IntoResponse;
/// Perform the extraction.
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection>;
fn from_request(
req: Request,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}
#[async_trait]
impl<S, T> FromRequest<S, private::ViaParts> for T
where
S: Send + Sync,
@ -99,7 +102,6 @@ where
}
}
#[async_trait]
impl<S, T> FromRequestParts<S> for Option<T>
where
T: FromRequestParts<S>,
@ -115,7 +117,6 @@ where
}
}
#[async_trait]
impl<S, T> FromRequest<S> for Option<T>
where
T: FromRequest<S>,
@ -128,7 +129,6 @@ where
}
}
#[async_trait]
impl<S, T> FromRequestParts<S> for Result<T, T::Rejection>
where
T: FromRequestParts<S>,
@ -141,7 +141,6 @@ where
}
}
#[async_trait]
impl<S, T> FromRequest<S> for Result<T, T::Rejection>
where
T: FromRequest<S>,

View file

@ -42,7 +42,7 @@ define_rejection! {
#[body = "Failed to buffer the request body"]
/// Encountered some other error when buffering the body.
///
/// This can _only_ happen when you're using [`tower_http::limit::RequestBodyLimitLayer`] or
/// This can _only_ happen when you're using [`tower_http::limit::RequestBodyLimitLayer`] or
/// otherwise wrapping request bodies in [`http_body_util::Limited`].
pub struct LengthLimitError(Error);
}

View file

@ -1,12 +1,10 @@
use super::{rejection::*, FromRequest, FromRequestParts, Request};
use crate::{body::Body, RequestExt};
use async_trait::async_trait;
use bytes::Bytes;
use bytes::{BufMut, Bytes, BytesMut};
use http::{request::Parts, Extensions, HeaderMap, Method, Uri, Version};
use http_body_util::BodyExt;
use std::convert::Infallible;
#[async_trait]
impl<S> FromRequest<S> for Request
where
S: Send + Sync,
@ -18,7 +16,6 @@ where
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Method
where
S: Send + Sync,
@ -30,7 +27,6 @@ where
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Uri
where
S: Send + Sync,
@ -42,7 +38,6 @@ where
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Version
where
S: Send + Sync,
@ -59,7 +54,6 @@ where
/// Prefer using [`TypedHeader`] to extract only the headers you need.
///
/// [`TypedHeader`]: https://docs.rs/axum/0.7/axum/extract/struct.TypedHeader.html
#[async_trait]
impl<S> FromRequestParts<S> for HeaderMap
where
S: Send + Sync,
@ -71,7 +65,36 @@ where
}
}
#[async_trait]
impl<S> FromRequest<S> for BytesMut
where
S: Send + Sync,
{
type Rejection = BytesRejection;
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
let mut body = req.into_limited_body();
let mut bytes = BytesMut::new();
body_to_bytes_mut(&mut body, &mut bytes).await?;
Ok(bytes)
}
}
async fn body_to_bytes_mut(body: &mut Body, bytes: &mut BytesMut) -> Result<(), BytesRejection> {
while let Some(frame) = body
.frame()
.await
.transpose()
.map_err(FailedToBufferBody::from_err)?
{
let Ok(data) = frame.into_data() else {
return Ok(());
};
bytes.put(data);
}
Ok(())
}
impl<S> FromRequest<S> for Bytes
where
S: Send + Sync,
@ -90,7 +113,6 @@ where
}
}
#[async_trait]
impl<S> FromRequest<S> for String
where
S: Send + Sync,
@ -106,15 +128,12 @@ where
}
})?;
let string = std::str::from_utf8(&bytes)
.map_err(InvalidUtf8::from_err)?
.to_owned();
let string = String::from_utf8(bytes.into()).map_err(InvalidUtf8::from_err)?;
Ok(string)
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Parts
where
S: Send + Sync,
@ -126,7 +145,6 @@ where
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Extensions
where
S: Send + Sync,
@ -138,7 +156,6 @@ where
}
}
#[async_trait]
impl<S> FromRequest<S> for Body
where
S: Send + Sync,

View file

@ -1,10 +1,8 @@
use super::{FromRequest, FromRequestParts, Request};
use crate::response::{IntoResponse, Response};
use async_trait::async_trait;
use http::request::Parts;
use std::convert::Infallible;
#[async_trait]
impl<S> FromRequestParts<S> for ()
where
S: Send + Sync,
@ -20,7 +18,6 @@ macro_rules! impl_from_request {
(
[$($ty:ident),*], $last:ident
) => {
#[async_trait]
#[allow(non_snake_case, unused_mut, unused_variables)]
impl<S, $($ty,)* $last> FromRequestParts<S> for ($($ty,)* $last,)
where
@ -46,7 +43,6 @@ macro_rules! impl_from_request {
// This impl must not be generic over M, otherwise it would conflict with the blanket
// implementation of `FromRequest<S, Mut>` for `T: FromRequestParts<S>`.
#[async_trait]
#[allow(non_snake_case, unused_mut, unused_variables)]
impl<S, $($ty,)* $last> FromRequest<S> for ($($ty,)* $last,)
where

View file

@ -1,4 +1,3 @@
#![cfg_attr(nightly_error_messages, feature(diagnostic_namespace))]
//! Core types and traits for [`axum`].
//!
//! Libraries authors that want to provide [`FromRequest`] or [`IntoResponse`] implementations
@ -22,7 +21,6 @@
clippy::needless_borrow,
clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock,
clippy::match_on_vec_items,
clippy::imprecise_flops,
@ -52,6 +50,11 @@
#[macro_use]
pub(crate) mod macros;
#[doc(hidden)] // macro helpers
pub mod __private {
#[cfg(feature = "tracing")]
pub use tracing;
}
mod error;
mod ext_traits;

View file

@ -1,4 +1,5 @@
/// Private API.
#[cfg(feature = "tracing")]
#[doc(hidden)]
#[macro_export]
macro_rules! __log_rejection {
@ -7,20 +8,30 @@ macro_rules! __log_rejection {
body_text = $body_text:expr,
status = $status:expr,
) => {
#[cfg(feature = "tracing")]
{
tracing::event!(
$crate::__private::tracing::event!(
target: "axum::rejection",
tracing::Level::TRACE,
$crate::__private::tracing::Level::TRACE,
status = $status.as_u16(),
body = $body_text,
rejection_type = std::any::type_name::<$ty>(),
rejection_type = ::std::any::type_name::<$ty>(),
"rejecting request",
);
}
};
}
#[cfg(not(feature = "tracing"))]
#[doc(hidden)]
#[macro_export]
macro_rules! __log_rejection {
(
rejection_type = $ty:ident,
body_text = $body_text:expr,
status = $status:expr,
) => {};
}
/// Private API.
#[doc(hidden)]
#[macro_export]
@ -303,8 +314,6 @@ mod composite_rejection_tests {
#[allow(dead_code, unreachable_pub)]
mod defs {
use crate::{__composite_rejection, __define_rejection};
__define_rejection! {
#[status = BAD_REQUEST]
#[body = "error message 1"]

View file

@ -29,7 +29,7 @@ use std::fmt;
/// )
/// }
/// ```
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct AppendHeaders<I>(pub I);

View file

@ -49,7 +49,7 @@ use std::{
/// MyError::SomethingElseWentWrong => "something else went wrong",
/// };
///
/// // its often easiest to implement `IntoResponse` by calling other implementations
/// // it's often easiest to implement `IntoResponse` by calling other implementations
/// (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
/// }
/// }
@ -113,6 +113,7 @@ use std::{
/// ```
pub trait IntoResponse {
/// Create a response.
#[must_use]
fn into_response(self) -> Response;
}

View file

@ -44,7 +44,7 @@ use std::{convert::Infallible, fmt};
/// }
/// }
///
/// // Its also recommended to implement `IntoResponse` so `SetHeader` can be used on its own as
/// // It's also recommended to implement `IntoResponse` so `SetHeader` can be used on its own as
/// // the response
/// impl<'a> IntoResponse for SetHeader<'a> {
/// fn into_response(self) -> Response {
@ -105,21 +105,25 @@ pub struct ResponseParts {
impl ResponseParts {
/// Gets a reference to the response headers.
#[must_use]
pub fn headers(&self) -> &HeaderMap {
self.res.headers()
}
/// Gets a mutable reference to the response headers.
#[must_use]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
self.res.headers_mut()
}
/// Gets a reference to the response extensions.
#[must_use]
pub fn extensions(&self) -> &Extensions {
self.res.extensions()
}
/// Gets a mutable reference to the response extensions.
#[must_use]
pub fn extensions_mut(&mut self) -> &mut Extensions {
self.res.extensions_mut()
}
@ -260,3 +264,11 @@ impl IntoResponseParts for Extensions {
Ok(res)
}
}
impl IntoResponseParts for () {
type Error = Infallible;
fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
Ok(res)
}
}

View file

@ -121,6 +121,7 @@ where
///
/// See [`Result`] for more details.
#[derive(Debug)]
#[must_use]
pub struct ErrorResponse(Response);
impl<T> From<T> for ErrorResponse

View file

@ -7,7 +7,59 @@ and this project adheres to [Semantic Versioning].
# Unreleased
- None.
- **fixed:** `Host` extractor includes port number when parsing authority ([#2242])
- **added:** Add `RouterExt::typed_connect` ([#2961])
- **added:** Add `json!` for easy construction of JSON responses ([#2962])
[#2242]: https://github.com/tokio-rs/axum/pull/2242
[#2961]: https://github.com/tokio-rs/axum/pull/2961
[#2962]: https://github.com/tokio-rs/axum/pull/2962
# 0.10.0
# alpha.1
- **breaking:** Update to prost 0.13. Used for the `Protobuf` extractor ([#2829])
- **change:** Update minimum rust version to 1.75 ([#2943])
[#2829]: https://github.com/tokio-rs/axum/pull/2829
[#2943]: https://github.com/tokio-rs/axum/pull/2943
# 0.9.6
- **docs:** Add links to features table ([#3030])
[#3030]: https://github.com/tokio-rs/axum/pull/3030
# 0.9.5
- **added:** Add `RouterExt::typed_connect` ([#2961])
- **added:** Add `json!` for easy construction of JSON responses ([#2962])
[#2961]: https://github.com/tokio-rs/axum/pull/2961
[#2962]: https://github.com/tokio-rs/axum/pull/2962
# 0.9.4
- **added:** The `response::Attachment` type ([#2789])
[#2789]: https://github.com/tokio-rs/axum/pull/2789
# 0.9.3 (24. March, 2024)
- **added:** New `tracing` feature which enables logging rejections from
built-in extractor with the `axum::rejection=trace` target ([#2584])
[#2584]: https://github.com/tokio-rs/axum/pull/2584
# 0.9.2 (13. January, 2024)
- **added:** Implement `TypedPath` for `WithRejection<TypedPath, _>`
- **fixed:** Documentation link to `serde::Deserialize` in `JsonDeserializer` extractor ([#2498])
- **added:** Add `is_missing` function for `TypedHeaderRejection` and `TypedHeaderRejectionReason` ([#2503])
[#2498]: https://github.com/tokio-rs/axum/pull/2498
[#2503]: https://github.com/tokio-rs/axum/pull/2503
# 0.9.1 (29. December, 2023)

View file

@ -2,24 +2,26 @@
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Extra utilities for axum"
edition = "2021"
rust-version = "1.66"
rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
license = "MIT"
name = "axum-extra"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.9.1"
version = "0.10.0-alpha.1"
[features]
default = []
default = ["tracing", "multipart"]
async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"]
attachment = ["dep:tracing"]
error_response = ["dep:tracing", "tracing/std"]
cookie = ["dep:cookie"]
cookie-private = ["cookie", "cookie?/private"]
cookie-signed = ["cookie", "cookie?/signed"]
cookie-key-expansion = ["cookie", "cookie?/key-expansion"]
erased-json = ["dep:serde_json"]
erased-json = ["dep:serde_json", "dep:typed-json"]
form = ["dep:serde_html_form"]
json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"]
json-lines = [
@ -30,15 +32,17 @@ json-lines = [
"tokio-stream?/io-util",
"dep:tokio",
]
multipart = ["dep:multer"]
multipart = ["dep:multer", "dep:fastrand"]
protobuf = ["dep:prost"]
scheme = []
query = ["dep:serde_html_form"]
tracing = ["axum-core/tracing", "axum/tracing"]
typed-header = ["dep:headers"]
typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"]
[dependencies]
axum = { path = "../axum", version = "0.7.2", default-features = false }
axum-core = { path = "../axum-core", version = "0.4.2" }
axum = { path = "../axum", version = "0.8.0-alpha.1", default-features = false, features = ["original-uri"] }
axum-core = { path = "../axum-core", version = "0.5.0-alpha.1" }
bytes = "1.1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0"
@ -47,38 +51,41 @@ http-body-util = "0.1.0"
mime = "0.3"
pin-project-lite = "0.2"
serde = "1.0"
tower = { version = "0.4", default_features = false, features = ["util"] }
tower = { version = "0.5.1", default-features = false, features = ["util"] }
tower-layer = "0.3"
tower-service = "0.3"
# optional dependencies
axum-macros = { path = "../axum-macros", version = "0.4.0", optional = true }
axum-macros = { path = "../axum-macros", version = "0.5.0-alpha.1", optional = true }
cookie = { package = "cookie", version = "0.18.0", features = ["percent-encode"], optional = true }
fastrand = { version = "2.1.0", optional = true }
form_urlencoded = { version = "1.1.0", optional = true }
headers = { version = "0.4.0", optional = true }
multer = { version = "3.0.0", optional = true }
percent-encoding = { version = "2.1", optional = true }
prost = { version = "0.12", optional = true }
prost = { version = "0.13", optional = true }
serde_html_form = { version = "0.2.0", optional = true }
serde_json = { version = "1.0.71", optional = true }
serde_path_to_error = { version = "0.1.8", optional = true }
tokio = { version = "1.19", optional = true }
tokio-stream = { version = "0.1.9", optional = true }
tokio-util = { version = "0.7", optional = true }
tracing = { version = "0.1.37", default-features = false, optional = true }
typed-json = { version = "0.1.1", optional = true }
[dev-dependencies]
axum = { path = "../axum", version = "0.7.2" }
axum = { path = "../axum", features = ["macros"] }
axum-macros = { path = "../axum-macros", features = ["__private"] }
hyper = "1.0.0"
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "multipart"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.71"
tokio = { version = "1.14", features = ["full"] }
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.5.0", features = ["map-response-body", "timeout"] }
tower = { version = "0.5.1", features = ["util"] }
tower-http = { version = "0.6.0", features = ["map-response-body", "timeout"] }
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-public-api-crates]
allowed = [
@ -93,6 +100,7 @@ allowed = [
"headers_core",
"http",
"http_body",
"pin_project_lite",
"prost",
"serde",
"tokio",

View file

@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum-extra's MSRV is 1.66.
axum-extra's MSRV is 1.75.
## Getting Help

View file

@ -7,7 +7,6 @@
//! use axum::{
//! body::Bytes,
//! Router,
//! async_trait,
//! routing::get,
//! extract::FromRequestParts,
//! };
@ -15,7 +14,6 @@
//! // extractors for checking permissions
//! struct AdminPermissions {}
//!
//! #[async_trait]
//! impl<S> FromRequestParts<S> for AdminPermissions
//! where
//! S: Send + Sync,
@ -29,7 +27,6 @@
//!
//! struct User {}
//!
//! #[async_trait]
//! impl<S> FromRequestParts<S> for User
//! where
//! S: Send + Sync,
@ -96,7 +93,6 @@
use std::task::{Context, Poll};
use axum::{
async_trait,
extract::FromRequestParts,
response::{IntoResponse, Response},
};
@ -236,7 +232,6 @@ macro_rules! impl_traits_for_either {
[$($ident:ident),* $(,)?],
$last:ident $(,)?
) => {
#[async_trait]
impl<S, $($ident),*, $last> FromRequestParts<S> for $either<$($ident),*, $last>
where
$($ident: FromRequestParts<S>),*,
@ -247,12 +242,12 @@ macro_rules! impl_traits_for_either {
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
$(
if let Ok(value) = FromRequestParts::from_request_parts(parts, state).await {
if let Ok(value) = <$ident as FromRequestParts<S>>::from_request_parts(parts, state).await {
return Ok(Self::$ident(value));
}
)*
FromRequestParts::from_request_parts(parts, state).await.map(Self::$last)
<$last as FromRequestParts<S>>::from_request_parts(parts, state).await.map(Self::$last)
}
}

View file

@ -1,7 +1,4 @@
use axum::{
async_trait,
extract::{Extension, FromRequestParts},
};
use axum::extract::{Extension, FromRequestParts};
use http::request::Parts;
/// Cache results of other extractors.
@ -19,7 +16,6 @@ use http::request::Parts;
/// ```rust
/// use axum_extra::extract::Cached;
/// use axum::{
/// async_trait,
/// extract::FromRequestParts,
/// response::{IntoResponse, Response},
/// http::{StatusCode, request::Parts},
@ -28,7 +24,6 @@ use http::request::Parts;
/// #[derive(Clone)]
/// struct Session { /* ... */ }
///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for Session
/// where
/// S: Send + Sync,
@ -43,7 +38,6 @@ use http::request::Parts;
///
/// struct CurrentUser { /* ... */ }
///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for CurrentUser
/// where
/// S: Send + Sync,
@ -86,7 +80,6 @@ pub struct Cached<T>(pub T);
#[derive(Clone)]
struct CachedEntry<T>(T);
#[async_trait]
impl<S, T> FromRequestParts<S> for Cached<T>
where
S: Send + Sync,
@ -111,8 +104,7 @@ axum_core::__impl_deref!(Cached);
#[cfg(test)]
mod tests {
use super::*;
use axum::{extract::FromRequestParts, http::Request, routing::get, Router};
use http::request::Parts;
use axum::{http::Request, routing::get, Router};
use std::{
convert::Infallible,
sync::atomic::{AtomicU32, Ordering},
@ -126,7 +118,6 @@ mod tests {
#[derive(Clone, Debug, PartialEq, Eq)]
struct Extractor(Instant);
#[async_trait]
impl<S> FromRequestParts<S> for Extractor
where
S: Send + Sync,

View file

@ -3,7 +3,6 @@
//! See [`CookieJar`], [`SignedCookieJar`], and [`PrivateCookieJar`] for more details.
use axum::{
async_trait,
extract::FromRequestParts,
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
@ -90,7 +89,6 @@ pub struct CookieJar {
jar: cookie::CookieJar,
}
#[async_trait]
impl<S> FromRequestParts<S> for CookieJar
where
S: Send + Sync,

View file

@ -1,6 +1,5 @@
use super::{cookies_from_request, set_cookies, Cookie, Key};
use axum::{
async_trait,
extract::{FromRef, FromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
@ -49,11 +48,11 @@ use std::{convert::Infallible, fmt, marker::PhantomData};
/// // our application state
/// #[derive(Clone)]
/// struct AppState {
/// // that holds the key used to sign cookies
/// // that holds the key used to encrypt cookies
/// key: Key,
/// }
///
/// // this impl tells `SignedCookieJar` how to access the key from our state
/// // this impl tells `PrivateCookieJar` how to access the key from our state
/// impl FromRef<AppState> for Key {
/// fn from_ref(state: &AppState) -> Self {
/// state.key.clone()
@ -122,7 +121,6 @@ impl<K> fmt::Debug for PrivateCookieJar<K> {
}
}
#[async_trait]
impl<S, K> FromRequestParts<S> for PrivateCookieJar<K>
where
S: Send + Sync,
@ -291,7 +289,7 @@ struct PrivateCookieJarIter<'a, K> {
iter: cookie::Iter<'a>,
}
impl<'a, K> Iterator for PrivateCookieJarIter<'a, K> {
impl<K> Iterator for PrivateCookieJarIter<'_, K> {
type Item = Cookie<'static>;
fn next(&mut self) -> Option<Self::Item> {

View file

@ -1,6 +1,5 @@
use super::{cookies_from_request, set_cookies};
use axum::{
async_trait,
extract::{FromRef, FromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
@ -139,7 +138,6 @@ impl<K> fmt::Debug for SignedCookieJar<K> {
}
}
#[async_trait]
impl<S, K> FromRequestParts<S> for SignedCookieJar<K>
where
S: Send + Sync,
@ -309,7 +307,7 @@ struct SignedCookieJarIter<'a, K> {
iter: cookie::Iter<'a>,
}
impl<'a, K> Iterator for SignedCookieJarIter<'a, K> {
impl<K> Iterator for SignedCookieJarIter<'_, K> {
type Item = Cookie<'static>;
fn next(&mut self) -> Option<Self::Item> {

View file

@ -1,5 +1,4 @@
use axum::{
async_trait,
extract::{rejection::RawFormRejection, FromRequest, RawForm, Request},
response::{IntoResponse, Response},
Error, RequestExt,
@ -44,7 +43,6 @@ pub struct Form<T>(pub T);
axum_core::__impl_deref!(Form);
#[async_trait]
impl<T, S> FromRequest<S> for Form<T>
where
T: DeserializeOwned,
@ -81,11 +79,16 @@ impl IntoResponse for FormRejection {
fn into_response(self) -> Response {
match self {
Self::RawFormRejection(inner) => inner.into_response(),
Self::FailedToDeserializeForm(inner) => (
StatusCode::BAD_REQUEST,
format!("Failed to deserialize form: {inner}"),
)
.into_response(),
Self::FailedToDeserializeForm(inner) => {
let body = format!("Failed to deserialize form: {inner}");
let status = StatusCode::BAD_REQUEST;
axum_core::__log_rejection!(
rejection_type = Self,
body_text = body,
status = status,
);
(status, body).into_response()
}
}
}
}
@ -113,7 +116,7 @@ mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::post, Router};
use http::{header::CONTENT_TYPE, StatusCode};
use http::header::CONTENT_TYPE;
use serde::Deserialize;
#[tokio::test]
@ -135,7 +138,6 @@ mod tests {
.post("/")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("value=one&value=two")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);

View file

@ -1,29 +1,29 @@
use super::{
rejection::{FailedToResolveHost, HostRejection},
FromRequestParts,
};
use async_trait::async_trait;
use super::rejection::{FailedToResolveHost, HostRejection};
use axum::extract::FromRequestParts;
use http::{
header::{HeaderMap, FORWARDED},
request::Parts,
uri::Authority,
};
const X_FORWARDED_HOST_HEADER_KEY: &str = "X-Forwarded-Host";
/// Extractor that resolves the hostname of the request.
/// Extractor that resolves the host of the request.
///
/// Hostname is resolved through the following, in order:
/// Host is resolved through the following, in order:
/// - `Forwarded` header
/// - `X-Forwarded-Host` header
/// - `Host` header
/// - request target / URI
/// - Authority of the request URI
///
/// See <https://www.rfc-editor.org/rfc/rfc9110.html#name-host-and-authority> for the definition of
/// host.
///
/// Note that user agents can set `X-Forwarded-Host` and `Host` headers to arbitrary values so make
/// sure to validate them to avoid security issues.
#[derive(Debug, Clone)]
pub struct Host(pub String);
#[async_trait]
impl<S> FromRequestParts<S> for Host
where
S: Send + Sync,
@ -51,8 +51,8 @@ where
return Ok(Host(host.to_owned()));
}
if let Some(host) = parts.uri.host() {
return Ok(Host(host.to_owned()));
if let Some(authority) = parts.uri.authority() {
return Ok(Host(parse_authority(authority).to_owned()));
}
Err(HostRejection::FailedToResolveHost(FailedToResolveHost))
@ -76,11 +76,19 @@ fn parse_forwarded(headers: &HeaderMap) -> Option<&str> {
})
}
fn parse_authority(auth: &Authority) -> &str {
auth.as_str()
.rsplit('@')
.next()
.expect("split always has at least 1 item")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{routing::get, test_helpers::TestClient, Router};
use http::header::HeaderName;
use crate::test_helpers::TestClient;
use axum::{routing::get, Router};
use http::{header::HeaderName, Request};
fn test_client() -> TestClient {
async fn host_as_body(Host(host): Host) -> String {
@ -96,7 +104,6 @@ mod tests {
let host = test_client()
.get("/")
.header(http::header::HOST, original_host)
.send()
.await
.text()
.await;
@ -109,7 +116,6 @@ mod tests {
let host = test_client()
.get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, original_host)
.send()
.await
.text()
.await;
@ -124,7 +130,6 @@ mod tests {
.get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header)
.header(http::header::HOST, host_header)
.send()
.await
.text()
.await;
@ -133,8 +138,26 @@ mod tests {
#[crate::test]
async fn uri_host() {
let host = test_client().get("/").send().await.text().await;
assert!(host.contains("127.0.0.1"));
let client = test_client();
let port = client.server_port();
let host = client.get("/").await.text().await;
assert_eq!(host, format!("127.0.0.1:{port}"));
}
#[crate::test]
async fn ip4_uri_host() {
let mut parts = Request::new(()).into_parts().0;
parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap();
let host = Host::from_request_parts(&mut parts, &()).await.unwrap();
assert_eq!(host.0, "127.0.0.1:1234");
}
#[crate::test]
async fn ip6_uri_host() {
let mut parts = Request::new(()).into_parts().0;
parts.uri = "http://cool:user@[::1]:456/file.txt".parse().unwrap();
let host = Host::from_request_parts(&mut parts, &()).await.unwrap();
assert_eq!(host.0, "[::1]:456");
}
#[test]

View file

@ -1,4 +1,3 @@
use axum::async_trait;
use axum::extract::{FromRequest, Request};
use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
@ -10,7 +9,7 @@ use std::marker::PhantomData;
/// JSON Extractor for zero-copy deserialization.
///
/// Deserialize request bodies into some type that implements [`serde::Deserialize<'de>`].
/// Deserialize request bodies into some type that implements [`serde::Deserialize<'de>`][serde::Deserialize].
/// Parsing JSON is delayed until [`deserialize`](JsonDeserializer::deserialize) is called.
/// If the type implements [`serde::de::DeserializeOwned`], the [`Json`](axum::Json) extractor should
/// be preferred.
@ -23,8 +22,7 @@ use std::marker::PhantomData;
/// Additionally, a `JsonRejection` error will be returned, when calling `deserialize` if:
///
/// - The body doesn't contain syntactically valid JSON.
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target
/// type.
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target type.
/// - Attempting to deserialize escaped JSON into a type that must be borrowed (e.g. `&'a str`).
///
/// ⚠️ `serde` will implicitly try to borrow for `&str` and `&[u8]` types, but will error if the
@ -85,7 +83,6 @@ pub struct JsonDeserializer<T> {
_marker: PhantomData<T>,
}
#[async_trait]
impl<T, S> FromRequest<S> for JsonDeserializer<T>
where
T: Deserialize<'static>,
@ -205,7 +202,7 @@ fn json_content_type(headers: &HeaderMap) -> bool {
};
let is_json_content_type = mime.type_() == "application"
&& (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json"));
&& (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"));
is_json_content_type
}
@ -245,7 +242,7 @@ mod tests {
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client.post("/").json(&json!({ "foo": "bar" })).send().await;
let res = client.post("/").json(&json!({ "foo": "bar" })).await;
let body = res.text().await;
assert_eq!(body, "bar");
@ -277,11 +274,7 @@ mod tests {
let client = TestClient::new(app);
// The escaped characters prevent serde_json from borrowing.
let res = client
.post("/")
.json(&json!({ "foo": "\"bar\"" }))
.send()
.await;
let res = client.post("/").json(&json!({ "foo": "\"bar\"" })).await;
let body = res.text().await;
@ -308,19 +301,11 @@ mod tests {
let client = TestClient::new(app);
let res = client
.post("/")
.json(&json!({ "foo": "good" }))
.send()
.await;
let res = client.post("/").json(&json!({ "foo": "good" })).await;
let body = res.text().await;
assert_eq!(body, "good");
let res = client
.post("/")
.json(&json!({ "foo": "\"bad\"" }))
.send()
.await;
let res = client.post("/").json(&json!({ "foo": "\"bad\"" })).await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let body_text = res.text().await;
assert_eq!(
@ -344,7 +329,7 @@ mod tests {
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client.post("/").body(r#"{ "foo": "bar" }"#).send().await;
let res = client.post("/").body(r#"{ "foo": "bar" }"#).await;
let status = res.status();
@ -366,7 +351,6 @@ mod tests {
.post("/")
.header("content-type", content_type)
.body("{}")
.send()
.await;
res.status() == StatusCode::OK
@ -395,7 +379,6 @@ mod tests {
.post("/")
.body("{")
.header("content-type", "application/json")
.send()
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@ -433,7 +416,6 @@ mod tests {
.post("/")
.body("{\"a\": 1, \"b\": [{\"x\": 2}]}")
.header("content-type", "application/json")
.send()
.await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);

View file

@ -1,7 +1,9 @@
//! Additional extractors.
mod cached;
mod host;
mod optional_path;
pub mod rejection;
mod with_rejection;
#[cfg(feature = "form")]
@ -19,7 +21,12 @@ mod query;
#[cfg(feature = "multipart")]
pub mod multipart;
pub use self::{cached::Cached, optional_path::OptionalPath, with_rejection::WithRejection};
#[cfg(feature = "scheme")]
mod scheme;
pub use self::{
cached::Cached, host::Host, optional_path::OptionalPath, with_rejection::WithRejection,
};
#[cfg(feature = "cookie")]
pub use self::cookie::CookieJar;
@ -39,6 +46,10 @@ pub use self::query::{OptionalQuery, OptionalQueryRejection, Query, QueryRejecti
#[cfg(feature = "multipart")]
pub use self::multipart::Multipart;
#[cfg(feature = "scheme")]
#[doc(no_inline)]
pub use self::scheme::{Scheme, SchemeMissing};
#[cfg(feature = "json-deserializer")]
pub use self::json_deserializer::{
JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError,

View file

@ -3,7 +3,6 @@
//! See [`Multipart`] for more details.
use axum::{
async_trait,
body::{Body, Bytes},
extract::FromRequest,
response::{IntoResponse, Response},
@ -75,7 +74,7 @@ use std::{
/// to keep `Field`s around from previous loop iterations. That will minimize the risk of runtime
/// errors.
///
/// # Differences between this and `axum::extract::Multipart`
/// # Differences between this and `axum::extract::Multipart`
///
/// `axum::extract::Multipart` uses lifetimes to enforce field exclusivity at compile time, however
/// that leads to significant usability issues such as `Field` not being `'static`.
@ -90,7 +89,6 @@ pub struct Multipart {
inner: multer::Multipart<'static>,
}
#[async_trait]
impl<S> FromRequest<S> for Multipart
where
S: Send + Sync,
@ -379,7 +377,13 @@ pub struct InvalidBoundary;
impl IntoResponse for InvalidBoundary {
fn into_response(self) -> Response {
(self.status(), self.body_text()).into_response()
let body = self.body_text();
axum_core::__log_rejection!(
rejection_type = Self,
body_text = body,
status = self.status(),
);
(self.status(), body).into_response()
}
}
@ -407,7 +411,7 @@ impl std::error::Error for InvalidBoundary {}
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{extract::DefaultBodyLimit, response::IntoResponse, routing::post, Router};
use axum::{extract::DefaultBodyLimit, routing::post, Router};
#[tokio::test]
async fn content_type_with_encoding() {
@ -437,7 +441,7 @@ mod tests {
.unwrap(),
);
client.post("/").multipart(form).send().await;
client.post("/").multipart(form).await;
}
// No need for this to be a #[test], we just want to make sure it compiles
@ -466,7 +470,7 @@ mod tests {
let form =
reqwest::multipart::Form::new().part("file", reqwest::multipart::Part::bytes(BYTES));
let res = client.post("/").multipart(form).send().await;
let res = client.post("/").multipart(form).await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
}

View file

@ -1,5 +1,4 @@
use axum::{
async_trait,
extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Path},
RequestPartsExt,
};
@ -29,13 +28,12 @@ use serde::de::DeserializeOwned;
///
/// let app = Router::new()
/// .route("/blog", get(render_blog))
/// .route("/blog/:page", get(render_blog));
/// .route("/blog/{page}", get(render_blog));
/// # let app: Router = app;
/// ```
#[derive(Debug)]
pub struct OptionalPath<T>(pub Option<T>);
#[async_trait]
impl<T, S> FromRequestParts<S> for OptionalPath<T>
where
T: DeserializeOwned + Send + 'static,
@ -77,26 +75,26 @@ mod tests {
let app = Router::new()
.route("/", get(handle))
.route("/:num", get(handle));
.route("/{num}", get(handle));
let client = TestClient::new(app);
let res = client.get("/").send().await;
let res = client.get("/").await;
assert_eq!(res.text().await, "Success: 0");
let res = client.get("/1").send().await;
let res = client.get("/1").await;
assert_eq!(res.text().await, "Success: 1");
let res = client.get("/0").send().await;
let res = client.get("/0").await;
assert_eq!(
res.text().await,
"Invalid URL: invalid value: integer `0`, expected a nonzero u32"
);
let res = client.get("/NaN").send().await;
let res = client.get("/NaN").await;
assert_eq!(
res.text().await,
"Invalid URL: Cannot parse `\"NaN\"` to a `u32`"
"Invalid URL: Cannot parse `NaN` to a `u32`"
);
}
}

View file

@ -1,5 +1,4 @@
use axum::{
async_trait,
extract::FromRequestParts,
response::{IntoResponse, Response},
Error,
@ -51,11 +50,37 @@ use std::fmt;
/// example.
///
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
///
/// While `Option<T>` will handle empty parameters (e.g. `param=`), beware when using this with a
/// `Vec<T>`. If your list is optional, use `Vec<T>` in combination with `#[serde(default)]`
/// instead of `Option<Vec<T>>`. `Option<Vec<T>>` will handle 0, 2, or more arguments, but not one
/// argument.
///
/// # Example
///
/// ```rust,no_run
/// use axum::{routing::get, Router};
/// use axum_extra::extract::Query;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Params {
/// #[serde(default)]
/// items: Vec<usize>,
/// }
///
/// // This will parse 0 occurrences of `items` as an empty `Vec`.
/// async fn process_items(Query(params): Query<Params>) {
/// // ...
/// }
///
/// let app = Router::new().route("/process_items", get(process_items));
/// # let _: Router = app;
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Query<T>(pub T);
#[async_trait]
impl<T, S> FromRequestParts<S> for Query<T>
where
T: DeserializeOwned,
@ -87,11 +112,16 @@ pub enum QueryRejection {
impl IntoResponse for QueryRejection {
fn into_response(self) -> Response {
match self {
Self::FailedToDeserializeQueryString(inner) => (
StatusCode::BAD_REQUEST,
format!("Failed to deserialize query string: {inner}"),
)
.into_response(),
Self::FailedToDeserializeQueryString(inner) => {
let body = format!("Failed to deserialize query string: {inner}");
let status = StatusCode::BAD_REQUEST;
axum_core::__log_rejection!(
rejection_type = Self,
body_text = body,
status = status,
);
(status, body).into_response()
}
}
}
}
@ -155,7 +185,6 @@ impl std::error::Error for QueryRejection {
#[derive(Debug, Clone, Copy, Default)]
pub struct OptionalQuery<T>(pub Option<T>);
#[async_trait]
impl<T, S> FromRequestParts<S> for OptionalQuery<T>
where
T: DeserializeOwned,
@ -235,7 +264,7 @@ mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::post, Router};
use http::{header::CONTENT_TYPE, StatusCode};
use http::header::CONTENT_TYPE;
use serde::Deserialize;
#[tokio::test]
@ -257,7 +286,6 @@ mod tests {
.post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
@ -286,7 +314,6 @@ mod tests {
.post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
@ -312,7 +339,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.post("/").body("").send().await;
let res = client.post("/").body("").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "None");
@ -341,7 +368,6 @@ mod tests {
.post("/?other=something")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.send()
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);

View file

@ -0,0 +1,23 @@
//! Rejection response types.
use axum_core::{
__composite_rejection as composite_rejection, __define_rejection as define_rejection,
};
define_rejection! {
#[status = BAD_REQUEST]
#[body = "No host found in request"]
/// Rejection type used if the [`Host`](super::Host) extractor is unable to
/// resolve a host.
pub struct FailedToResolveHost;
}
composite_rejection! {
/// Rejection used for [`Host`](super::Host).
///
/// Contains one variant for each way the [`Host`](super::Host) extractor
/// can fail.
pub enum HostRejection {
FailedToResolveHost,
}
}

View file

@ -0,0 +1,152 @@
//! Extractor that parses the scheme of a request.
//! See [`Scheme`] for more details.
use axum::{
extract::FromRequestParts,
response::{IntoResponse, Response},
};
use http::{
header::{HeaderMap, FORWARDED},
request::Parts,
};
const X_FORWARDED_PROTO_HEADER_KEY: &str = "X-Forwarded-Proto";
/// Extractor that resolves the scheme / protocol of a request.
///
/// The scheme is resolved through the following, in order:
/// - `Forwarded` header
/// - `X-Forwarded-Proto` header
/// - Request URI (If the request is an HTTP/2 request! e.g. use `--http2(-prior-knowledge)` with cURL)
///
/// Note that user agents can set the `X-Forwarded-Proto` header to arbitrary values so make
/// sure to validate them to avoid security issues.
#[derive(Debug, Clone)]
pub struct Scheme(pub String);
/// Rejection type used if the [`Scheme`] extractor is unable to
/// resolve a scheme.
#[derive(Debug)]
pub struct SchemeMissing;
impl IntoResponse for SchemeMissing {
fn into_response(self) -> Response {
(http::StatusCode::BAD_REQUEST, "No scheme found in request").into_response()
}
}
impl<S> FromRequestParts<S> for Scheme
where
S: Send + Sync,
{
type Rejection = SchemeMissing;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// Within Forwarded header
if let Some(scheme) = parse_forwarded(&parts.headers) {
return Ok(Scheme(scheme.to_owned()));
}
// X-Forwarded-Proto
if let Some(scheme) = parts
.headers
.get(X_FORWARDED_PROTO_HEADER_KEY)
.and_then(|scheme| scheme.to_str().ok())
{
return Ok(Scheme(scheme.to_owned()));
}
// From parts of an HTTP/2 request
if let Some(scheme) = parts.uri.scheme_str() {
return Ok(Scheme(scheme.to_owned()));
}
Err(SchemeMissing)
}
}
fn parse_forwarded(headers: &HeaderMap) -> Option<&str> {
// if there are multiple `Forwarded` `HeaderMap::get` will return the first one
let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?;
// get the first set of values
let first_value = forwarded_values.split(',').next()?;
// find the value of the `proto` field
first_value.split(';').find_map(|pair| {
let (key, value) = pair.split_once('=')?;
key.trim()
.eq_ignore_ascii_case("proto")
.then(|| value.trim().trim_matches('"'))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::TestClient;
use axum::{routing::get, Router};
use http::header::HeaderName;
fn test_client() -> TestClient {
async fn scheme_as_body(Scheme(scheme): Scheme) -> String {
scheme
}
TestClient::new(Router::new().route("/", get(scheme_as_body)))
}
#[crate::test]
async fn forwarded_scheme_parsing() {
// the basic case
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "http");
// is case insensitive
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;PROTO=https;by=203.0.113.43")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "https");
// multiple values in one header
let headers = header_map(&[(FORWARDED, "proto=ftp, proto=https")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "ftp");
// multiple header values
let headers = header_map(&[(FORWARDED, "proto=ftp"), (FORWARDED, "proto=https")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "ftp");
}
#[crate::test]
async fn x_forwarded_scheme_header() {
let original_scheme = "https";
let scheme = test_client()
.get("/")
.header(X_FORWARDED_PROTO_HEADER_KEY, original_scheme)
.await
.text()
.await;
assert_eq!(scheme, original_scheme);
}
#[crate::test]
async fn precedence_forwarded_over_x_forwarded() {
let scheme = test_client()
.get("/")
.header(X_FORWARDED_PROTO_HEADER_KEY, "https")
.header(FORWARDED, "proto=ftp")
.await
.text()
.await;
assert_eq!(scheme, "ftp");
}
fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap {
let mut headers = HeaderMap::new();
for (key, value) in values {
headers.append(key, value.parse().unwrap());
}
headers
}
}

View file

@ -1,11 +1,13 @@
use axum::async_trait;
use axum::extract::{FromRequest, FromRequestParts, Request};
use axum::response::IntoResponse;
use http::request::Parts;
use std::fmt::Debug;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
#[cfg(feature = "typed-routing")]
use crate::routing::TypedPath;
/// Extractor for customizing extractor rejections
///
/// `WithRejection` wraps another extractor and gives you the result. If the
@ -107,7 +109,6 @@ impl<E, R> DerefMut for WithRejection<E, R> {
}
}
#[async_trait]
impl<E, R, S> FromRequest<S> for WithRejection<E, R>
where
S: Send + Sync,
@ -122,7 +123,6 @@ where
}
}
#[async_trait]
impl<E, R, S> FromRequestParts<S> for WithRejection<E, R>
where
S: Send + Sync,
@ -137,22 +137,35 @@ where
}
}
#[cfg(feature = "typed-routing")]
impl<E, R> TypedPath for WithRejection<E, R>
where
E: TypedPath,
{
const PATH: &'static str = E::PATH;
}
impl<E, R> Display for WithRejection<E, R>
where
E: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::extract::FromRequestParts;
use axum::http::Request;
use axum::response::Response;
use http::request::Parts;
use super::*;
#[tokio::test]
async fn extractor_rejection_is_transformed() {
struct TestExtractor;
struct TestRejection;
#[async_trait]
impl<S> FromRequestParts<S> for TestExtractor
where
S: Send + Sync,

View file

@ -47,7 +47,6 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// use axum_extra::handler::HandlerCallWithExtractors;
/// use axum::{
/// Router,
/// async_trait,
/// routing::get,
/// extract::FromRequestParts,
/// };
@ -68,7 +67,6 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// // extractors for checking permissions
/// struct AdminPermissions {}
///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for AdminPermissions
/// where
/// S: Send + Sync,
@ -82,7 +80,6 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
///
/// struct User {}
///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for User
/// where
/// S: Send + Sync,
@ -95,7 +92,7 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// }
///
/// let app = Router::new().route(
/// "/users/:id",
/// "/users/{id}",
/// get(
/// // first try `admin`, if that rejects run `user`, finally falling back
/// // to `guest`
@ -168,7 +165,7 @@ pub struct IntoHandler<H, T, S> {
impl<H, T, S> Handler<T, S> for IntoHandler<H, T, S>
where
H: HandlerCallWithExtractors<T, S> + Clone + Send + 'static,
H: HandlerCallWithExtractors<T, S> + Clone + Send + Sync + 'static,
T: FromRequest<S> + Send + 'static,
T::Rejection: Send,
S: Send + Sync + 'static,

View file

@ -54,8 +54,8 @@ where
impl<S, L, R, Lt, Rt, M> Handler<(M, Lt, Rt), S> for Or<L, R, Lt, Rt, S>
where
L: HandlerCallWithExtractors<Lt, S> + Clone + Send + 'static,
R: HandlerCallWithExtractors<Rt, S> + Clone + Send + 'static,
L: HandlerCallWithExtractors<Lt, S> + Clone + Send + Sync + 'static,
R: HandlerCallWithExtractors<Rt, S> + Clone + Send + Sync + 'static,
Lt: FromRequestParts<S> + Send + 'static,
Rt: FromRequest<S, M> + Send + 'static,
Lt::Rejection: Send,
@ -134,17 +134,17 @@ mod tests {
"fallback"
}
let app = Router::new().route("/:id", get(one.or(two).or(three)));
let app = Router::new().route("/{id}", get(one.or(two).or(three)));
let client = TestClient::new(app);
let res = client.get("/123").send().await;
let res = client.get("/123").await;
assert_eq!(res.text().await, "123");
let res = client.get("/foo?a=bar").send().await;
let res = client.get("/foo?a=bar").await;
assert_eq!(res.text().await, "bar");
let res = client.get("/foo").send().await;
let res = client.get("/foo").await;
assert_eq!(res.text().await, "fallback");
}
}

View file

@ -1,7 +1,6 @@
//! Newline delimited JSON extractor and response.
use axum::{
async_trait,
body::Body,
extract::{FromRequest, Request},
response::{IntoResponse, Response},
@ -55,7 +54,7 @@ pin_project! {
/// JsonLines::new(stream_of_values()).into_response()
/// }
/// ```
// we use `AsExtractor` as the default because you're more likely to name this type if its used
// we use `AsExtractor` as the default because you're more likely to name this type if it's used
// as an extractor
#[must_use]
pub struct JsonLines<S, T = AsExtractor> {
@ -99,7 +98,6 @@ impl<S> JsonLines<S, AsResponse> {
}
}
#[async_trait]
impl<S, T> FromRequest<S> for JsonLines<T, AsExtractor>
where
T: DeserializeOwned,
@ -184,7 +182,7 @@ mod tests {
use futures_util::StreamExt;
use http::StatusCode;
use serde::Deserialize;
use std::{convert::Infallible, error::Error};
use std::error::Error;
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct User {
@ -224,7 +222,6 @@ mod tests {
]
.join("\n"),
)
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -245,7 +242,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/").send().await;
let res = client.get("/").await;
let values = res
.text()

View file

@ -9,20 +9,22 @@
//!
//! Name | Description | Default?
//! ---|---|---
//! `async-read-body` | Enables the `AsyncReadBody` body | No
//! `cookie` | Enables the `CookieJar` extractor | No
//! `cookie-private` | Enables the `PrivateCookieJar` extractor | No
//! `cookie-signed` | Enables the `SignedCookieJar` extractor | No
//! `cookie-key-expansion` | Enables the `Key::derive_from` method | No
//! `erased-json` | Enables the `ErasedJson` response | No
//! `form` | Enables the `Form` extractor | No
//! `json-deserializer` | Enables the `JsonDeserializer` extractor | No
//! `json-lines` | Enables the `JsonLines` extractor and response | No
//! `multipart` | Enables the `Multipart` extractor | No
//! `protobuf` | Enables the `Protobuf` extractor and response | No
//! `query` | Enables the `Query` extractor | No
//! `typed-routing` | Enables the `TypedPath` routing utilities | No
//! `typed-header` | Enables the `TypedHeader` extractor and response | No
//! `async-read-body` | Enables the [`AsyncReadBody`](crate::body::AsyncReadBody) body | No
//! `attachment` | Enables the [`Attachment`](crate::response::Attachment) response | No
//! `cookie` | Enables the [`CookieJar`](crate::extract::CookieJar) extractor | No
//! `cookie-private` | Enables the [`PrivateCookieJar`](crate::extract::PrivateCookieJar) extractor | No
//! `cookie-signed` | Enables the [`SignedCookieJar`](crate::extract::SignedCookieJar) extractor | No
//! `cookie-key-expansion` | Enables the [`Key::derive_from`](crate::extract::cookie::Key::derive_from) method | No
//! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response | No
//! `form` | Enables the [`Form`](crate::extract::Form) extractor | No
//! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor | No
//! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response | No
//! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor | No
//! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response | No
//! `query` | Enables the [`Query`](crate::extract::Query) extractor | No
//! `tracing` | Log rejections from built-in extractors | Yes
//! `typed-routing` | Enables the [`TypedPath`](crate::routing::TypedPath) routing utilities | No
//! `typed-header` | Enables the [`TypedHeader`] extractor and response | No
//!
//! [`axum`]: https://crates.io/crates/axum
@ -39,7 +41,6 @@
clippy::needless_borrow,
clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock,
clippy::match_on_vec_items,
clippy::imprecise_flops,
@ -96,11 +97,10 @@ pub use typed_header::TypedHeader;
#[cfg(feature = "protobuf")]
pub mod protobuf;
/// _not_ public API
#[cfg(feature = "typed-routing")]
#[doc(hidden)]
pub mod __private {
//! _not_ public API
use percent_encoding::{AsciiSet, CONTROLS};
pub use percent_encoding::utf8_percent_encode;
@ -115,9 +115,8 @@ pub mod __private {
use axum_macros::__private_axum_test as test;
#[cfg(test)]
#[allow(unused_imports)]
pub(crate) mod test_helpers {
#![allow(unused_imports)]
use axum::{extract::Request, response::Response, serve};
mod test_client {

View file

@ -1,7 +1,6 @@
//! Protocol Buffer extractor and response.
use axum::{
async_trait,
extract::{rejection::BytesRejection, FromRequest, Request},
response::{IntoResponse, IntoResponseFailed, Response},
};
@ -82,7 +81,7 @@ use prost::Message;
/// # unimplemented!()
/// }
///
/// let app = Router::new().route("/users/:id", get(get_user));
/// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app;
/// ```
#[derive(Debug, Clone, Copy, Default)]
@ -90,7 +89,6 @@ use prost::Message;
#[must_use]
pub struct Protobuf<T>(pub T);
#[async_trait]
impl<T, S> FromRequest<S> for Protobuf<T>
where
T: Message + Default,
@ -206,7 +204,6 @@ mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::post, Router};
use http::StatusCode;
#[tokio::test]
async fn decode_body() {
@ -226,7 +223,7 @@ mod tests {
};
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).send().await;
let res = client.post("/").body(input.encode_to_vec()).await;
let body = res.text().await;
@ -254,7 +251,7 @@ mod tests {
};
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).send().await;
let res = client.post("/").body(input.encode_to_vec()).await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
@ -289,7 +286,7 @@ mod tests {
};
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).send().await;
let res = client.post("/").body(input.encode_to_vec()).await;
assert_eq!(
res.headers()["content-type"],

View file

@ -0,0 +1,103 @@
use axum::response::IntoResponse;
use http::{header, HeaderMap, HeaderValue};
use tracing::error;
/// A file attachment response.
///
/// This type will set the `Content-Disposition` header to `attachment`. In response a webbrowser
/// will offer to download the file instead of displaying it directly.
///
/// Use the `filename` and `content_type` methods to set the filename or content-type of the
/// attachment. If these values are not set they will not be sent.
///
///
/// # Example
///
/// ```rust
/// use axum::{http::StatusCode, routing::get, Router};
/// use axum_extra::response::Attachment;
///
/// async fn cargo_toml() -> Result<Attachment<String>, (StatusCode, String)> {
/// let file_contents = tokio::fs::read_to_string("Cargo.toml")
/// .await
/// .map_err(|err| (StatusCode::NOT_FOUND, format!("File not found: {err}")))?;
/// Ok(Attachment::new(file_contents)
/// .filename("Cargo.toml")
/// .content_type("text/x-toml"))
/// }
///
/// let app = Router::new().route("/Cargo.toml", get(cargo_toml));
/// let _: Router = app;
/// ```
///
/// # Note
///
/// If you use axum with hyper, hyper will set the `Content-Length` if it is known.
#[derive(Debug)]
#[must_use]
pub struct Attachment<T> {
inner: T,
filename: Option<HeaderValue>,
content_type: Option<HeaderValue>,
}
impl<T: IntoResponse> Attachment<T> {
/// Creates a new [`Attachment`].
pub fn new(inner: T) -> Self {
Self {
inner,
filename: None,
content_type: None,
}
}
/// Sets the filename of the [`Attachment`].
///
/// This updates the `Content-Disposition` header to add a filename.
pub fn filename<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
self.filename = if let Ok(filename) = value.try_into() {
Some(filename)
} else {
error!("Attachment filename contains invalid characters");
None
};
self
}
/// Sets the content-type of the [`Attachment`]
pub fn content_type<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
if let Ok(content_type) = value.try_into() {
self.content_type = Some(content_type);
} else {
error!("Attachment content-type contains invalid characters");
}
self
}
}
impl<T> IntoResponse for Attachment<T>
where
T: IntoResponse,
{
fn into_response(self) -> axum::response::Response {
let mut headers = HeaderMap::new();
if let Some(content_type) = self.content_type {
headers.append(header::CONTENT_TYPE, content_type);
}
let content_disposition = if let Some(filename) = self.filename {
let mut bytes = b"attachment; filename=\"".to_vec();
bytes.extend_from_slice(filename.as_bytes());
bytes.push(b'\"');
HeaderValue::from_bytes(&bytes).expect("This was a HeaderValue so this can not fail")
} else {
HeaderValue::from_static("attachment")
};
headers.append(header::CONTENT_DISPOSITION, content_disposition);
(headers, self.inner).into_response()
}
}

View file

@ -12,6 +12,15 @@ use serde::Serialize;
/// This allows returning a borrowing type from a handler, or returning different response
/// types as JSON from different branches inside a handler.
///
/// Like [`axum::Json`],
/// if the [`Serialize`] implementation fails
/// or if a map with non-string keys is used,
/// a 500 response will be issued
/// whose body is the error message in UTF-8.
///
/// This can be constructed using [`new`](ErasedJson::new)
/// or the [`json!`](crate::json) macro.
///
/// # Example
///
/// ```rust
@ -77,3 +86,65 @@ impl IntoResponse for ErasedJson {
}
}
}
/// Construct an [`ErasedJson`] response from a JSON literal.
///
/// A `Content-Type: application/json` header is automatically added.
/// Any variable or expression implementing [`Serialize`]
/// can be interpolated as a value in the literal.
/// If the [`Serialize`] implementation fails,
/// or if a map with non-string keys is used,
/// a 500 response will be issued
/// whose body is the error message in UTF-8.
///
/// Internally,
/// this function uses the [`typed_json::json!`] macro,
/// allowing it to perform far fewer allocations
/// than a dynamic macro like [`serde_json::json!`] would
/// it's equivalent to if you had just written
/// `derive(Serialize)` on a struct.
///
/// # Examples
///
/// ```
/// use axum::{
/// Router,
/// extract::Path,
/// response::Response,
/// routing::get,
/// };
/// use axum_extra::response::ErasedJson;
///
/// async fn get_user(Path(user_id) : Path<u64>) -> ErasedJson {
/// let user_name = find_user_name(user_id).await;
/// axum_extra::json!({ "name": user_name })
/// }
///
/// async fn find_user_name(user_id: u64) -> String {
/// // ...
/// # unimplemented!()
/// }
///
/// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app;
/// ```
///
/// Trailing commas are allowed in both arrays and objects.
///
/// ```
/// let response = axum_extra::json!(["trailing",]);
/// ```
#[macro_export]
macro_rules! json {
($($t:tt)*) => {
$crate::response::ErasedJson::new(
$crate::response::__private_erased_json::typed_json::json!($($t)*)
)
}
}
/// Not public API. Re-exported as `crate::response::__private_erased_json`.
#[doc(hidden)]
pub mod private {
pub use typed_json;
}

View file

@ -0,0 +1,51 @@
use axum_core::response::{IntoResponse, Response};
use http::StatusCode;
use std::error::Error;
use tracing::error;
/// Convenience response to create an error response from a non-[`IntoResponse`] error
///
/// This provides a method to quickly respond with an error that does not implement
/// the `IntoResponse` trait itself. This type should only be used for debugging purposes or internal
/// facing applications, as it includes the full error chain with descriptions,
/// thus leaking information that could possibly be sensitive.
///
/// ```rust
/// use axum_extra::response::InternalServerError;
/// use axum_core::response::IntoResponse;
/// # use std::io::{Error, ErrorKind};
/// # fn try_thing() -> Result<(), Error> {
/// # Err(Error::new(ErrorKind::Other, "error"))
/// # }
///
/// async fn maybe_error() -> Result<String, InternalServerError<Error>> {
/// try_thing().map_err(InternalServerError)?;
/// // do something on success
/// # Ok(String::from("ok"))
/// }
/// ```
#[derive(Debug)]
pub struct InternalServerError<T>(pub T);
impl<T: Error + 'static> IntoResponse for InternalServerError<T> {
fn into_response(self) -> Response {
error!(error = &self.0 as &dyn Error);
(
StatusCode::INTERNAL_SERVER_ERROR,
"An error occurred while processing your request.",
)
.into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Error, ErrorKind};
#[test]
fn internal_server_error() {
let response = InternalServerError(Error::new(ErrorKind::Other, "Test")).into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
}

View file

@ -3,13 +3,33 @@
#[cfg(feature = "erased-json")]
mod erased_json;
#[cfg(feature = "attachment")]
mod attachment;
#[cfg(feature = "multipart")]
pub mod multiple;
#[cfg(feature = "error_response")]
mod error_response;
#[cfg(feature = "error_response")]
pub use error_response::InternalServerError;
#[cfg(feature = "erased-json")]
pub use erased_json::ErasedJson;
/// _not_ public API
#[cfg(feature = "erased-json")]
#[doc(hidden)]
pub use erased_json::private as __private_erased_json;
#[cfg(feature = "json-lines")]
#[doc(no_inline)]
pub use crate::json_lines::JsonLines;
#[cfg(feature = "attachment")]
pub use attachment::Attachment;
macro_rules! mime_response {
(
$(#[$m:meta])*
@ -57,14 +77,6 @@ macro_rules! mime_response {
};
}
mime_response! {
/// A HTML response.
///
/// Will automatically get `Content-Type: text/html; charset=utf-8`.
Html,
TEXT_HTML_UTF_8,
}
mime_response! {
/// A JavaScript response.
///

View file

@ -0,0 +1,295 @@
//! Generate forms to use in responses.
use axum::response::{IntoResponse, Response};
use fastrand;
use http::{header, HeaderMap, StatusCode};
use mime::Mime;
/// Create multipart forms to be used in API responses.
///
/// This struct implements [`IntoResponse`], and so it can be returned from a handler.
#[derive(Debug)]
pub struct MultipartForm {
parts: Vec<Part>,
}
impl MultipartForm {
/// Initialize a new multipart form with the provided vector of parts.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// let parts: Vec<Part> = vec![Part::text("foo".to_string(), "abc"), Part::text("bar".to_string(), "def")];
/// let form = MultipartForm::with_parts(parts);
/// ```
pub fn with_parts(parts: Vec<Part>) -> Self {
MultipartForm { parts }
}
}
impl IntoResponse for MultipartForm {
fn into_response(self) -> Response {
// see RFC5758 for details
let boundary = generate_boundary();
let mut headers = HeaderMap::new();
let mime_type: Mime = match format!("multipart/form-data; boundary={}", boundary).parse() {
Ok(m) => m,
// Realistically this should never happen unless the boundary generation code
// is modified, and that will be caught by unit tests
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Invalid multipart boundary generated",
)
.into_response()
}
};
// The use of unwrap is safe here because mime types are inherently string representable
headers.insert(header::CONTENT_TYPE, mime_type.to_string().parse().unwrap());
let mut serialized_form: Vec<u8> = Vec::new();
for part in self.parts {
// for each part, the boundary is preceded by two dashes
serialized_form.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
serialized_form.extend_from_slice(&part.serialize());
}
serialized_form.extend_from_slice(format!("--{}--", boundary).as_bytes());
(headers, serialized_form).into_response()
}
}
// Valid settings for that header are: "base64", "quoted-printable", "8bit", "7bit", and "binary".
/// A single part of a multipart form as defined by
/// <https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4>
/// and RFC5758.
#[derive(Debug)]
pub struct Part {
// Every part is expected to contain:
// - a [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
// header, where `Content-Disposition` is set to `form-data`, with a parameter of `name` that is set to
// the name of the field in the form. In the below example, the name of the field is `user`:
// ```
// Content-Disposition: form-data; name="user"
// ```
// If the field contains a file, then the `filename` parameter may be set to the name of the file.
// Handling for non-ascii field names is not done here, support for non-ascii characters may be encoded using
// methodology described in RFC 2047.
// - (optionally) a `Content-Type` header, which if not set, defaults to `text/plain`.
// If the field contains a file, then the file should be identified with that file's MIME type (eg: `image/gif`).
// If the `MIME` type is not known or specified, then the MIME type should be set to `application/octet-stream`.
/// The name of the part in question
name: String,
/// If the part should be treated as a file, the filename that should be attached that part
filename: Option<String>,
/// The `Content-Type` header. While not strictly required, it is always set here
mime_type: Mime,
/// The content/body of the part
contents: Vec<u8>,
}
impl Part {
/// Create a new part with `Content-Type` of `text/plain` with the supplied name and contents.
///
/// This form will not have a defined file name.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// // create a form with a single part that has a field with a name of "foo",
/// // and a value of "abc"
/// let parts: Vec<Part> = vec![Part::text("foo".to_string(), "abc")];
/// let form = MultipartForm::from_iter(parts);
/// ```
pub fn text(name: String, contents: &str) -> Self {
Self {
name,
filename: None,
mime_type: mime::TEXT_PLAIN_UTF_8,
contents: contents.as_bytes().to_vec(),
}
}
/// Create a new part containing a generic file, with a `Content-Type` of `application/octet-stream`
/// using the provided file name, field name, and contents.
///
/// If the MIME type of the file is known, consider using `Part::raw_part`.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// // create a form with a single part that has a field with a name of "foo",
/// // with a file name of "foo.txt", and with the specified contents
/// let parts: Vec<Part> = vec![Part::file("foo", "foo.txt", vec![0x68, 0x68, 0x20, 0x6d, 0x6f, 0x6d])];
/// let form = MultipartForm::from_iter(parts);
/// ```
pub fn file(field_name: &str, file_name: &str, contents: Vec<u8>) -> Self {
Self {
name: field_name.to_owned(),
filename: Some(file_name.to_owned()),
// If the `MIME` type is not known or specified, then the MIME type should be set to `application/octet-stream`.
// See RFC2388 section 3 for specifics.
mime_type: mime::APPLICATION_OCTET_STREAM,
contents,
}
}
/// Create a new part with more fine-grained control over the semantics of that part.
///
/// The caller is assumed to have set a valid MIME type.
///
/// This function will return an error if the provided MIME type is not valid.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// // create a form with a single part that has a field with a name of "part_name",
/// // with a MIME type of "application/json", and the supplied contents.
/// let parts: Vec<Part> = vec![Part::raw_part("part_name", "application/json", vec![0x68, 0x68, 0x20, 0x6d, 0x6f, 0x6d], None).expect("MIME type must be valid")];
/// let form = MultipartForm::from_iter(parts);
/// ```
pub fn raw_part(
name: &str,
mime_type: &str,
contents: Vec<u8>,
filename: Option<&str>,
) -> Result<Self, &'static str> {
let mime_type = mime_type.parse().map_err(|_| "Invalid MIME type")?;
Ok(Self {
name: name.to_owned(),
filename: filename.map(|f| f.to_owned()),
mime_type,
contents,
})
}
/// Serialize this part into a chunk that can be easily inserted into a larger form
pub(super) fn serialize(&self) -> Vec<u8> {
// A part is serialized in this general format:
// // the filename is optional
// Content-Disposition: form-data; name="FIELD_NAME"; filename="FILENAME"\r\n
// // the mime type (not strictly required by the spec, but always sent here)
// Content-Type: mime/type\r\n
// // a blank line, then the contents of the file start
// \r\n
// CONTENTS\r\n
// Format what we can as a string, then handle the rest at a byte level
let mut serialized_part = format!("Content-Disposition: form-data; name=\"{}\"", self.name);
// specify a filename if one was set
if let Some(filename) = &self.filename {
serialized_part += &format!("; filename=\"{}\"", filename);
}
serialized_part += "\r\n";
// specify the MIME type
serialized_part += &format!("Content-Type: {}\r\n", self.mime_type);
serialized_part += "\r\n";
let mut part_bytes = serialized_part.as_bytes().to_vec();
part_bytes.extend_from_slice(&self.contents);
part_bytes.extend_from_slice(b"\r\n");
part_bytes
}
}
impl FromIterator<Part> for MultipartForm {
fn from_iter<T: IntoIterator<Item = Part>>(iter: T) -> Self {
Self {
parts: iter.into_iter().collect(),
}
}
}
/// A boundary is defined as a user defined (arbitrary) value that does not occur in any of the data.
///
/// Because the specification does not clearly define a methodology for generating boundaries, this implementation
/// follow's Reqwest's, and generates a boundary in the format of `XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX` where `XXXXXXXX`
/// is a hexadecimal representation of a pseudo randomly generated u64.
fn generate_boundary() -> String {
let a = fastrand::u64(0..u64::MAX);
let b = fastrand::u64(0..u64::MAX);
let c = fastrand::u64(0..u64::MAX);
let d = fastrand::u64(0..u64::MAX);
format!("{a:016x}-{b:016x}-{c:016x}-{d:016x}")
}
#[cfg(test)]
mod tests {
use super::{generate_boundary, MultipartForm, Part};
use axum::{body::Body, http};
use axum::{routing::get, Router};
use http::{Request, Response};
use http_body_util::BodyExt;
use mime::Mime;
use tower::ServiceExt;
#[tokio::test]
async fn process_form() -> Result<(), Box<dyn std::error::Error>> {
// create a boilerplate handle that returns a form
async fn handle() -> MultipartForm {
let parts: Vec<Part> = vec![
Part::text("part1".to_owned(), "basictext"),
Part::file(
"part2",
"file.txt",
vec![0x68, 0x69, 0x20, 0x6d, 0x6f, 0x6d],
),
Part::raw_part("part3", "text/plain", b"rawpart".to_vec(), None).unwrap(),
];
MultipartForm::from_iter(parts)
}
// make a request to that handle
let app = Router::new().route("/", get(handle));
let response: Response<_> = app
.oneshot(Request::builder().uri("/").body(Body::empty())?)
.await?;
// content_type header
let ct_header = response.headers().get("content-type").unwrap().to_str()?;
let boundary = ct_header.split("boundary=").nth(1).unwrap().to_owned();
let body: &[u8] = &response.into_body().collect().await?.to_bytes();
assert_eq!(
std::str::from_utf8(body)?,
&format!(
"--{boundary}\r\n\
Content-Disposition: form-data; name=\"part1\"\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
\r\n\
basictext\r\n\
--{boundary}\r\n\
Content-Disposition: form-data; name=\"part2\"; filename=\"file.txt\"\r\n\
Content-Type: application/octet-stream\r\n\
\r\n\
hi mom\r\n\
--{boundary}\r\n\
Content-Disposition: form-data; name=\"part3\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
rawpart\r\n\
--{boundary}--",
boundary = boundary
)
);
Ok(())
}
#[test]
fn valid_boundary_generation() {
for _ in 0..256 {
let boundary = generate_boundary();
let mime_type: Result<Mime, _> =
format!("multipart/form-data; boundary={}", boundary).parse();
assert!(
mime_type.is_ok(),
"The generated boundary was unable to be parsed into a valid mime type."
);
}
}
}

View file

@ -1,7 +1,7 @@
//! Additional types for defining routes.
use axum::{
extract::Request,
extract::{OriginalUri, Request},
response::{IntoResponse, Redirect, Response},
routing::{any, MethodRouter},
Router,
@ -131,6 +131,19 @@ pub trait RouterExt<S>: sealed::Sealed {
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `CONNECT` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_connect<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add another route to the router with an additional "trailing slash redirect" route.
///
/// If you add a route _without_ a trailing slash, such as `/foo`, this method will also add a
@ -165,7 +178,7 @@ pub trait RouterExt<S>: sealed::Sealed {
/// This works like [`RouterExt::route_with_tsr`] but accepts any [`Service`].
fn route_service_with_tsr<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + 'static,
T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
Self: Sized;
@ -255,6 +268,16 @@ where
self.route(P::PATH, axum::routing::trace(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_connect<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::connect(handler))
}
#[track_caller]
fn route_with_tsr(mut self, path: &str, method_router: MethodRouter<S>) -> Self
where
@ -268,7 +291,7 @@ where
#[track_caller]
fn route_service_with_tsr<T>(mut self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + 'static,
T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
Self: Sized,
@ -290,7 +313,7 @@ fn add_tsr_redirect_route<S>(router: Router<S>, path: &str) -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
async fn redirect_handler(uri: Uri) -> Response {
async fn redirect_handler(OriginalUri(uri): OriginalUri) -> Response {
let new_uri = map_path(uri, |path| {
path.strip_suffix('/')
.map(Cow::Borrowed)
@ -342,7 +365,7 @@ mod sealed {
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{extract::Path, http::StatusCode, routing::get};
use axum::{extract::Path, routing::get};
#[tokio::test]
async fn test_tsr() {
@ -352,17 +375,17 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
let res = client.get("/foo").await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.get("/foo/").send().await;
let res = client.get("/foo/").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/foo");
let res = client.get("/bar/").send().await;
let res = client.get("/bar/").await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.get("/bar").send().await;
let res = client.get("/bar").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/bar/");
}
@ -371,29 +394,29 @@ mod tests {
async fn tsr_with_params() {
let app = Router::new()
.route_with_tsr(
"/a/:a",
"/a/{a}",
get(|Path(param): Path<String>| async move { param }),
)
.route_with_tsr(
"/b/:b/",
"/b/{b}/",
get(|Path(param): Path<String>| async move { param }),
);
let client = TestClient::new(app);
let res = client.get("/a/foo").send().await;
let res = client.get("/a/foo").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "foo");
let res = client.get("/a/foo/").send().await;
let res = client.get("/a/foo/").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/a/foo");
let res = client.get("/b/foo/").send().await;
let res = client.get("/b/foo/").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "foo");
let res = client.get("/b/foo").send().await;
let res = client.get("/b/foo").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/b/foo/");
}
@ -404,11 +427,27 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/?a=a").send().await;
let res = client.get("/foo/?a=a").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/foo?a=a");
}
#[tokio::test]
async fn tsr_works_in_nested_router() {
let app = Router::new().nest(
"/neko",
Router::new().route_with_tsr("/nyan/", get(|| async {})),
);
let client = TestClient::new(app);
let res = client.get("/neko/nyan/").await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.get("/neko/nyan").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/neko/nyan/");
}
#[test]
#[should_panic = "Cannot add a trailing slash redirect route for `/`"]
fn tsr_at_root() {

View file

@ -19,13 +19,13 @@ use axum::{
/// .create(|| async {})
/// // `GET /users/new`
/// .new(|| async {})
/// // `GET /users/:users_id`
/// // `GET /users/{users_id}`
/// .show(|Path(user_id): Path<u64>| async {})
/// // `GET /users/:users_id/edit`
/// // `GET /users/{users_id}/edit`
/// .edit(|Path(user_id): Path<u64>| async {})
/// // `PUT or PATCH /users/:users_id`
/// // `PUT or PATCH /users/{users_id}`
/// .update(|Path(user_id): Path<u64>| async {})
/// // `DELETE /users/:users_id`
/// // `DELETE /users/{users_id}`
/// .destroy(|Path(user_id): Path<u64>| async {});
///
/// let app = Router::new().merge(users);
@ -82,7 +82,9 @@ where
self.route(&path, get(handler))
}
/// Add a handler at `GET /{resource_name}/:{resource_name}_id`.
/// Add a handler at `GET /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `GET /post/{post_id}`.
pub fn show<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
@ -92,17 +94,21 @@ where
self.route(&path, get(handler))
}
/// Add a handler at `GET /{resource_name}/:{resource_name}_id/edit`.
/// Add a handler at `GET /<resource_name>/{<resource_name>_id}/edit`.
///
/// For example when the resources are posts: `GET /post/{post_id}/edit`.
pub fn edit<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = format!("/{0}/:{0}_id/edit", self.name);
let path = format!("/{0}/{{{0}_id}}/edit", self.name);
self.route(&path, get(handler))
}
/// Add a handler at `PUT or PATCH /resource_name/:{resource_name}_id`.
/// Add a handler at `PUT or PATCH /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `PUT /post/{post_id}`.
pub fn update<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
@ -115,7 +121,9 @@ where
)
}
/// Add a handler at `DELETE /{resource_name}/:{resource_name}_id`.
/// Add a handler at `DELETE /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `DELETE /post/{post_id}`.
pub fn destroy<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
@ -130,7 +138,7 @@ where
}
fn show_update_destroy_path(&self) -> String {
format!("/{0}/:{0}_id", self.name)
format!("/{0}/{{{0}_id}}", self.name)
}
fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
@ -149,7 +157,7 @@ impl<S> From<Resource<S>> for Router<S> {
mod tests {
#[allow(unused_imports)]
use super::*;
use axum::{body::Body, extract::Path, http::Method, Router};
use axum::{body::Body, extract::Path, http::Method};
use http::Request;
use http_body_util::BodyExt;
use tower::ServiceExt;

View file

@ -19,15 +19,15 @@ use serde::Serialize;
/// RouterExt, // for `Router::typed_*`
/// };
///
/// // A type safe route with `/users/:id` as its associated path.
/// // A type safe route with `/users/{id}` as its associated path.
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: u32,
/// }
///
/// // A regular handler function that takes `UsersMember` as the first argument
/// // and thus creates a typed connection between this handler and the `/users/:id` path.
/// // and thus creates a typed connection between this handler and the `/users/{id}` path.
/// //
/// // The `TypedPath` must be the first argument to the function.
/// async fn users_show(
@ -39,7 +39,7 @@ use serde::Serialize;
/// let app = Router::new()
/// // Add our typed route to the router.
/// //
/// // The path will be inferred to `/users/:id` since `users_show`'s
/// // The path will be inferred to `/users/{id}` since `users_show`'s
/// // first argument is `UsersMember` which implements `TypedPath`
/// .typed_get(users_show)
/// .typed_post(users_create)
@ -75,7 +75,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: u32,
/// }
@ -85,12 +85,12 @@ use serde::Serialize;
///
/// - A `TypedPath` implementation.
/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`],
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
/// also implement [`serde::Deserialize`], unless it's a unit struct.
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
/// also implement [`serde::Deserialize`], unless it's a unit struct.
/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other
/// things, create links to known paths and have them verified statically. Note that the
/// [`Display`] implementation for each field must return something that's compatible with its
/// [`Deserialize`] implementation.
/// things, create links to known paths and have them verified statically. Note that the
/// [`Display`] implementation for each field must return something that's compatible with its
/// [`Deserialize`] implementation.
///
/// Additionally the macro will verify the captures in the path matches the fields of the struct.
/// For example this fails to compile since the struct doesn't have a `team_id` field:
@ -100,7 +100,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id/teams/:team_id")]
/// #[typed_path("/users/{id}/teams/{team_id}")]
/// struct UsersMember {
/// id: u32,
/// }
@ -117,7 +117,7 @@ use serde::Serialize;
/// struct UsersCollection;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember(u32);
/// ```
///
@ -130,7 +130,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: String,
/// }
@ -158,7 +158,7 @@ use serde::Serialize;
/// };
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id", rejection(UsersMemberRejection))]
/// #[typed_path("/users/{id}", rejection(UsersMemberRejection))]
/// struct UsersMember {
/// id: String,
/// }
@ -215,7 +215,7 @@ use serde::Serialize;
/// [`Deserialize`]: serde::Deserialize
/// [`PathRejection`]: axum::extract::rejection::PathRejection
pub trait TypedPath: std::fmt::Display {
/// The path with optional captures such as `/users/:id`.
/// The path with optional captures such as `/users/{id}`.
const PATH: &'static str;
/// Convert the path into a `Uri`.
@ -321,7 +321,7 @@ where
/// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
/// given type.
///
/// If you see it in type errors its most likely because the second argument to your handler doesn't
/// If you see it in type errors it's most likely because the second argument to your handler doesn't
/// implement [`TypedPath`].
///
/// You normally shouldn't have to use this trait directly.
@ -386,11 +386,19 @@ impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
#[cfg(test)]
mod tests {
use super::*;
use crate::routing::TypedPath;
use crate::{
extract::WithRejection,
routing::{RouterExt, TypedPath},
};
use axum::{
extract::rejection::PathRejection,
response::{IntoResponse, Response},
Router,
};
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct UsersShow {
id: i32,
}
@ -434,4 +442,25 @@ mod tests {
assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true&qux=1337");
}
#[allow(dead_code)] // just needs to compile
fn supports_with_rejection() {
async fn handler(_: WithRejection<UsersShow, MyRejection>) {}
struct MyRejection {}
impl IntoResponse for MyRejection {
fn into_response(self) -> Response {
unimplemented!()
}
}
impl From<PathRejection> for MyRejection {
fn from(_: PathRejection) -> Self {
unimplemented!()
}
}
let _: Router = Router::new().typed_get(handler);
}
}

View file

@ -1,12 +1,11 @@
//! Extractor and response for typed headers.
use axum::{
async_trait,
extract::FromRequestParts,
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
use headers::{Header, HeaderMapExt};
use http::request::Parts;
use http::{request::Parts, StatusCode};
use std::convert::Infallible;
/// Extractor and response that works with typed header values from [`headers`].
@ -30,7 +29,7 @@ use std::convert::Infallible;
/// // ...
/// }
///
/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
/// let app = Router::new().route("/users/{user_id}/team/{team_id}", get(users_teams_show));
/// # let _: Router = app;
/// ```
///
@ -55,7 +54,6 @@ use std::convert::Infallible;
#[must_use]
pub struct TypedHeader<T>(pub T);
#[async_trait]
impl<T, S> FromRequestParts<S> for TypedHeader<T>
where
T: Header,
@ -123,6 +121,14 @@ impl TypedHeaderRejection {
pub fn reason(&self) -> &TypedHeaderRejectionReason {
&self.reason
}
/// Returns `true` if the typed header rejection reason is [`Missing`].
///
/// [`Missing`]: TypedHeaderRejectionReason::Missing
#[must_use]
pub fn is_missing(&self) -> bool {
self.reason.is_missing()
}
}
/// Additional information regarding a [`TypedHeaderRejection`]
@ -136,9 +142,22 @@ pub enum TypedHeaderRejectionReason {
Error(headers::Error),
}
impl TypedHeaderRejectionReason {
/// Returns `true` if the typed header rejection reason is [`Missing`].
///
/// [`Missing`]: TypedHeaderRejectionReason::Missing
#[must_use]
pub fn is_missing(&self) -> bool {
matches!(self, Self::Missing)
}
}
impl IntoResponse for TypedHeaderRejection {
fn into_response(self) -> Response {
(http::StatusCode::BAD_REQUEST, self.to_string()).into_response()
let status = StatusCode::BAD_REQUEST;
let body = self.to_string();
axum_core::__log_rejection!(rejection_type = Self, body_text = body, status = status,);
(status, body).into_response()
}
}
@ -168,7 +187,7 @@ impl std::error::Error for TypedHeaderRejection {
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{response::IntoResponse, routing::get, Router};
use axum::{routing::get, Router};
#[tokio::test]
async fn typed_header() {
@ -190,7 +209,6 @@ mod tests {
.header("user-agent", "foobar")
.header("cookie", "a=1; b=2")
.header("cookie", "c=3")
.send()
.await;
let body = res.text().await;
assert_eq!(
@ -198,11 +216,11 @@ mod tests {
r#"User-Agent="foobar", Cookie=[("a", "1"), ("b", "2"), ("c", "3")]"#
);
let res = client.get("/").header("user-agent", "foobar").send().await;
let res = client.get("/").header("user-agent", "foobar").await;
let body = res.text().await;
assert_eq!(body, r#"User-Agent="foobar", Cookie=[]"#);
let res = client.get("/").header("cookie", "a=1").send().await;
let res = client.get("/").header("cookie", "a=1").await;
let body = res.text().await;
assert_eq!(body, "Header of type `user-agent` was missing");
}

View file

@ -5,9 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
# 0.5.0
- None.
## alpha.1
- **breaking:** Update code generation for axum-core 0.5.0-alpha.1
- **change:** Update minimum rust version to 1.75 ([#2943])
[#2943]: https://github.com/tokio-rs/axum/pull/2943
# 0.4.2
- **added:** Add `#[debug_middleware]` ([#1993], [#2725])
[#1993]: https://github.com/tokio-rs/axum/pull/1993
[#2725]: https://github.com/tokio-rs/axum/pull/2725
# 0.4.1 (13. January, 2024)
- **fixed:** Improve `debug_handler` on tuple response types ([#2201])
[#2201]: https://github.com/tokio-rs/axum/pull/2201
# 0.4.0 (27. November, 2023)

View file

@ -2,14 +2,14 @@
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Macros for axum"
edition = "2021"
rust-version = "1.66"
rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum"
keywords = ["axum"]
license = "MIT"
name = "axum-macros"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.4.0" # remember to also bump the version that axum and axum-extra depends on
version = "0.5.0-alpha.1" # remember to also bump the version that axum and axum-extra depends on
[features]
default = []
@ -19,7 +19,6 @@ __private = ["syn/visit-mut"]
proc-macro = true
[dependencies]
heck = "0.4"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = [
@ -30,8 +29,8 @@ syn = { version = "2.0", features = [
] }
[dev-dependencies]
axum = { path = "../axum", version = "0.7.2", features = ["macros"] }
axum-extra = { path = "../axum-extra", version = "0.9.0", features = ["typed-routing", "cookie-private", "typed-header"] }
axum = { path = "../axum", features = ["macros"] }
axum-extra = { path = "../axum-extra", features = ["typed-routing", "cookie-private", "typed-header"] }
rustversion = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -44,4 +43,3 @@ allowed = []
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View file

@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum-macros's MSRV is 1.66.
axum-macros's MSRV is 1.75.
## Getting Help

View file

@ -1 +1 @@
nightly-2023-09-23
nightly-2024-06-22

View file

@ -1,21 +1,26 @@
use std::collections::HashSet;
use std::{collections::HashSet, fmt};
use crate::{
attr_parsing::{parse_assignment_attribute, second},
with_position::{Position, WithPosition},
};
use proc_macro2::{Span, TokenStream};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, Token, Type};
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, ReturnType, Token, Type};
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenStream {
let Attrs { state_ty } = attr;
let mut state_ty = state_ty.map(second);
let check_extractor_count = check_extractor_count(&item_fn);
let check_path_extractor = check_path_extractor(&item_fn);
let check_output_impls_into_response = check_output_impls_into_response(&item_fn);
let check_extractor_count = check_extractor_count(&item_fn, kind);
let check_path_extractor = check_path_extractor(&item_fn, kind);
let check_output_tuples = check_output_tuples(&item_fn);
let check_output_impls_into_response = if check_output_tuples.is_empty() {
check_output_impls_into_response(&item_fn)
} else {
check_output_tuples
};
// If the function is generic, we can't reliably check its inputs or whether the future it
// returns is `Send`. Skip those checks to avoid unhelpful additional compiler errors.
@ -32,8 +37,10 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
err = Some(
syn::Error::new(
Span::call_site(),
"can't infer state type, please add set it explicitly, as in \
`#[debug_handler(state = MyStateType)]`",
format!(
"can't infer state type, please add set it explicitly, as in \
`#[axum_macros::debug_{kind}(state = MyStateType)]`"
),
)
.into_compile_error(),
);
@ -43,16 +50,16 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
err.unwrap_or_else(|| {
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(()));
let check_future_send = check_future_send(&item_fn);
let check_future_send = check_future_send(&item_fn, kind);
if let Some(check_input_order) = check_input_order(&item_fn) {
if let Some(check_input_order) = check_input_order(&item_fn, kind) {
quote! {
#check_input_order
#check_future_send
}
} else {
let check_inputs_impls_from_request =
check_inputs_impls_from_request(&item_fn, state_ty);
check_inputs_impls_from_request(&item_fn, state_ty, kind);
quote! {
#check_inputs_impls_from_request
@ -63,17 +70,45 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
} else {
syn::Error::new_spanned(
&item_fn.sig.generics,
"`#[axum_macros::debug_handler]` doesn't support generic functions",
format!("`#[axum_macros::debug_{kind}]` doesn't support generic functions"),
)
.into_compile_error()
};
let middleware_takes_next_as_last_arg =
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
quote! {
#item_fn
#check_extractor_count
#check_path_extractor
#check_output_impls_into_response
#check_inputs_and_future_send
#middleware_takes_next_as_last_arg
}
}
#[derive(Clone, Copy)]
pub(crate) enum FunctionKind {
Handler,
Middleware,
}
impl fmt::Display for FunctionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FunctionKind::Handler => f.write_str("handler"),
FunctionKind::Middleware => f.write_str("middleware"),
}
}
}
impl FunctionKind {
fn name_uppercase_plural(&self) -> &'static str {
match self {
FunctionKind::Handler => "Handlers",
FunctionKind::Middleware => "Middleware",
}
}
}
@ -105,25 +140,36 @@ impl Parse for Attrs {
}
}
fn check_extractor_count(item_fn: &ItemFn) -> Option<TokenStream> {
fn check_extractor_count(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
let max_extractors = 16;
if item_fn.sig.inputs.len() <= max_extractors {
let inputs = item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind))
.count();
if inputs <= max_extractors {
None
} else {
let error_message = format!(
"Handlers cannot take more than {max_extractors} arguments. \
"{} cannot take more than {max_extractors} arguments. \
Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors",
kind.name_uppercase_plural(),
);
let error = syn::Error::new_spanned(&item_fn.sig.inputs, error_message).to_compile_error();
Some(error)
}
}
fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
fn extractor_idents(
item_fn: &ItemFn,
kind: FunctionKind,
) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
item_fn
.sig
.inputs
.iter()
.filter(move |arg| skip_next_arg(arg, kind))
.enumerate()
.filter_map(|(idx, fn_arg)| match fn_arg {
FnArg::Receiver(_) => None,
@ -141,8 +187,8 @@ fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnAr
})
}
fn check_path_extractor(item_fn: &ItemFn) -> TokenStream {
let path_extractors = extractor_idents(item_fn)
fn check_path_extractor(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
let path_extractors = extractor_idents(item_fn, kind)
.filter(|(_, _, ident)| *ident == "Path")
.collect::<Vec<_>>();
@ -174,121 +220,294 @@ fn is_self_pat_type(typed: &syn::PatType) -> bool {
ident == "self"
}
fn check_inputs_impls_from_request(item_fn: &ItemFn, state_ty: Type) -> TokenStream {
let takes_self = item_fn.sig.inputs.first().map_or(false, |arg| match arg {
fn check_inputs_impls_from_request(
item_fn: &ItemFn,
state_ty: Type,
kind: FunctionKind,
) -> TokenStream {
let takes_self = item_fn.sig.inputs.first().is_some_and(|arg| match arg {
FnArg::Receiver(_) => true,
FnArg::Typed(typed) => is_self_pat_type(typed),
});
WithPosition::new(item_fn.sig.inputs.iter())
.enumerate()
.map(|(idx, arg)| {
let must_impl_from_request_parts = match &arg {
Position::First(_) | Position::Middle(_) => true,
Position::Last(_) | Position::Only(_) => false,
};
WithPosition::new(
item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind)),
)
.enumerate()
.map(|(idx, arg)| {
let must_impl_from_request_parts = match &arg {
Position::First(_) | Position::Middle(_) => true,
Position::Last(_) | Position::Only(_) => false,
};
let arg = arg.into_inner();
let arg = arg.into_inner();
let (span, ty) = match arg {
FnArg::Receiver(receiver) => {
if receiver.reference.is_some() {
return syn::Error::new_spanned(
receiver,
"Handlers must only take owned values",
)
.into_compile_error();
}
let (span, ty) = match arg {
FnArg::Receiver(receiver) => {
if receiver.reference.is_some() {
return syn::Error::new_spanned(
receiver,
"Handlers must only take owned values",
)
.into_compile_error();
}
let span = receiver.span();
let span = receiver.span();
(span, syn::parse_quote!(Self))
}
FnArg::Typed(typed) => {
let ty = &typed.ty;
let span = ty.span();
if is_self_pat_type(typed) {
(span, syn::parse_quote!(Self))
}
FnArg::Typed(typed) => {
let ty = &typed.ty;
let span = ty.span();
if is_self_pat_type(typed) {
(span, syn::parse_quote!(Self))
} else {
(span, ty.clone())
}
}
};
let consumes_request = request_consuming_type_name(&ty).is_some();
let check_fn = format_ident!(
"__axum_macros_check_{}_{}_from_request_check",
item_fn.sig.ident,
idx,
span = span,
);
let call_check_fn = format_ident!(
"__axum_macros_check_{}_{}_from_request_call_check",
item_fn.sig.ident,
idx,
span = span,
);
let call_check_fn_body = if takes_self {
quote_spanned! {span=>
Self::#check_fn();
}
} else {
quote_spanned! {span=>
#check_fn();
}
};
let check_fn_generics = if must_impl_from_request_parts || consumes_request {
quote! {}
} else {
quote! { <M> }
};
let from_request_bound = if must_impl_from_request_parts {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
}
} else if consumes_request {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequest<#state_ty> + Send
}
} else {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequest<#state_ty, M> + Send
}
};
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #check_fn #check_fn_generics()
where
#from_request_bound,
{}
// we have to call the function to actually trigger a compile error
// since the function is generic, just defining it is not enough
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #call_check_fn()
{
#call_check_fn_body
} else {
(span, ty.clone())
}
}
})
.collect::<TokenStream>()
};
let consumes_request = request_consuming_type_name(&ty).is_some();
let check_fn = format_ident!(
"__axum_macros_check_{}_{}_from_request_check",
item_fn.sig.ident,
idx,
span = span,
);
let call_check_fn = format_ident!(
"__axum_macros_check_{}_{}_from_request_call_check",
item_fn.sig.ident,
idx,
span = span,
);
let call_check_fn_body = if takes_self {
quote_spanned! {span=>
Self::#check_fn();
}
} else {
quote_spanned! {span=>
#check_fn();
}
};
let check_fn_generics = if must_impl_from_request_parts || consumes_request {
quote! {}
} else {
quote! { <M> }
};
let from_request_bound = if must_impl_from_request_parts {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
}
} else if consumes_request {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequest<#state_ty> + Send
}
} else {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequest<#state_ty, M> + Send
}
};
quote_spanned! {span=>
#[allow(warnings)]
#[doc(hidden)]
fn #check_fn #check_fn_generics()
where
#from_request_bound,
{}
// we have to call the function to actually trigger a compile error
// since the function is generic, just defining it is not enough
#[allow(warnings)]
#[doc(hidden)]
fn #call_check_fn()
{
#call_check_fn_body
}
}
})
.collect::<TokenStream>()
}
fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
fn check_output_tuples(item_fn: &ItemFn) -> TokenStream {
let elems = match &item_fn.sig.output {
ReturnType::Type(_, ty) => match &**ty {
Type::Tuple(tuple) => &tuple.elems,
_ => return quote! {},
},
ReturnType::Default => return quote! {},
};
let handler_ident = &item_fn.sig.ident;
match elems.len() {
0 => quote! {},
n if n > 17 => syn::Error::new_spanned(
&item_fn.sig.output,
"Cannot return tuples with more than 17 elements",
)
.to_compile_error(),
_ => WithPosition::new(elems)
.enumerate()
.map(|(idx, arg)| match arg {
Position::First(ty) => match extract_clean_typename(ty).as_deref() {
Some("StatusCode" | "Response") => quote! {},
Some("Parts") => check_is_response_parts(ty, handler_ident, idx),
Some(_) | None => {
if let Some(tn) = well_known_last_response_type(ty) {
syn::Error::new_spanned(
ty,
format!(
"`{tn}` must be the last element \
in a response tuple"
),
)
.to_compile_error()
} else {
check_into_response_parts(ty, handler_ident, idx)
}
}
},
Position::Middle(ty) => {
if let Some(tn) = well_known_last_response_type(ty) {
syn::Error::new_spanned(
ty,
format!("`{tn}` must be the last element in a response tuple"),
)
.to_compile_error()
} else {
check_into_response_parts(ty, handler_ident, idx)
}
}
Position::Last(ty) | Position::Only(ty) => check_into_response(handler_ident, ty),
})
.collect::<TokenStream>(),
}
}
fn check_into_response(handler: &Ident, ty: &Type) -> TokenStream {
let (span, ty) = (ty.span(), ty.clone());
let check_fn = format_ident!(
"__axum_macros_check_{handler}_into_response_check",
span = span,
);
let call_check_fn = format_ident!(
"__axum_macros_check_{handler}_into_response_call_check",
span = span,
);
let call_check_fn_body = quote_spanned! {span=>
#check_fn();
};
let from_request_bound = quote_spanned! {span=>
#ty: ::axum::response::IntoResponse
};
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #check_fn()
where
#from_request_bound,
{}
// we have to call the function to actually trigger a compile error
// since the function is generic, just defining it is not enough
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #call_check_fn() {
#call_check_fn_body
}
}
}
fn check_is_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream {
let (span, ty) = (ty.span(), ty.clone());
let check_fn = format_ident!(
"__axum_macros_check_{}_is_response_parts_{index}_check",
ident,
span = span,
);
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #check_fn(parts: #ty) -> ::axum::http::response::Parts {
parts
}
}
}
fn check_into_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream {
let (span, ty) = (ty.span(), ty.clone());
let check_fn = format_ident!(
"__axum_macros_check_{}_into_response_parts_{index}_check",
ident,
span = span,
);
let call_check_fn = format_ident!(
"__axum_macros_check_{}_into_response_parts_{index}_call_check",
ident,
span = span,
);
let call_check_fn_body = quote_spanned! {span=>
#check_fn();
};
let from_request_bound = quote_spanned! {span=>
#ty: ::axum::response::IntoResponseParts
};
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #check_fn()
where
#from_request_bound,
{}
// we have to call the function to actually trigger a compile error
// since the function is generic, just defining it is not enough
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #call_check_fn() {
#call_check_fn_body
}
}
}
fn check_input_order(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
let number_of_inputs = item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind))
.count();
let types_that_consume_the_request = item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind))
.enumerate()
.filter_map(|(idx, arg)| {
let ty = match arg {
@ -308,7 +527,7 @@ fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
// exactly one type that consumes the request
if types_that_consume_the_request.len() == 1 {
// and that is not the last
if types_that_consume_the_request[0].0 != item_fn.sig.inputs.len() - 1 {
if types_that_consume_the_request[0].0 != number_of_inputs - 1 {
let (_idx, type_name, span) = &types_that_consume_the_request[0];
let error = format!(
"`{type_name}` consumes the request body and thus must be \
@ -334,7 +553,7 @@ fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
compile_error!(#error);
})
} else {
let types = WithPosition::new(types_that_consume_the_request.into_iter())
let types = WithPosition::new(types_that_consume_the_request)
.map(|pos| match pos {
Position::First((_, type_name, _)) | Position::Middle((_, type_name, _)) => {
format!("`{type_name}`, ")
@ -355,18 +574,18 @@ fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
}
}
fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
fn extract_clean_typename(ty: &Type) -> Option<String> {
let path = match ty {
Type::Path(type_path) => &type_path.path,
_ => return None,
};
path.segments.last().map(|p| p.ident.to_string())
}
let ident = match path.segments.last() {
Some(path_segment) => &path_segment.ident,
None => return None,
};
fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
let typename = extract_clean_typename(ty)?;
let type_name = match &*ident.to_string() {
let type_name = match &*typename {
"Json" => "Json<_>",
"RawBody" => "RawBody<_>",
"RawForm" => "RawForm",
@ -384,6 +603,22 @@ fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
Some(type_name)
}
fn well_known_last_response_type(ty: &Type) -> Option<&'static str> {
let typename = extract_clean_typename(ty)?;
let type_name = match &*typename {
"Json" => "Json<_>",
"Protobuf" => "Protobuf",
"JsonLines" => "JsonLines<_>",
"Form" => "Form<_>",
"Bytes" => "Bytes",
"String" => "String",
_ => return None,
};
Some(type_name)
}
fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
let ty = match &item_fn.sig.output {
syn::ReturnType::Default => return quote! {},
@ -473,13 +708,13 @@ fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
}
}
fn check_future_send(item_fn: &ItemFn) -> TokenStream {
fn check_future_send(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
if item_fn.sig.asyncness.is_none() {
match &item_fn.sig.output {
syn::ReturnType::Default => {
return syn::Error::new_spanned(
item_fn.sig.fn_token,
"Handlers must be `async fn`s",
format!("{} must be `async fn`s", kind.name_uppercase_plural()),
)
.into_compile_error();
}
@ -583,7 +818,69 @@ fn state_types_from_args(item_fn: &ItemFn) -> HashSet<Type> {
crate::infer_state_types(types).collect()
}
fn next_is_last_input(item_fn: &ItemFn) -> TokenStream {
let next_args = item_fn
.sig
.inputs
.iter()
.enumerate()
.filter(|(_, arg)| !skip_next_arg(arg, FunctionKind::Middleware))
.collect::<Vec<_>>();
if next_args.is_empty() {
return quote! {
compile_error!(
"Middleware functions must take `axum::middleware::Next` as the last argument",
);
};
}
if next_args.len() == 1 {
let (idx, arg) = &next_args[0];
if *idx != item_fn.sig.inputs.len() - 1 {
return quote_spanned! {arg.span()=>
compile_error!("`axum::middleware::Next` must the last argument");
};
}
}
if next_args.len() >= 2 {
return quote! {
compile_error!(
"Middleware functions can only take one argument of type `axum::middleware::Next`",
);
};
}
quote! {}
}
fn skip_next_arg(arg: &FnArg, kind: FunctionKind) -> bool {
match kind {
FunctionKind::Handler => true,
FunctionKind::Middleware => match arg {
FnArg::Receiver(_) => true,
FnArg::Typed(pat_type) => {
if let Type::Path(type_path) = &*pat_type.ty {
type_path
.path
.segments
.last()
.map_or(true, |path_segment| path_segment.ident != "Next")
} else {
true
}
}
},
}
}
#[test]
fn ui() {
fn ui_debug_handler() {
crate::run_ui_tests("debug_handler");
}
#[test]
fn ui_debug_middleware() {
crate::run_ui_tests("debug_middleware");
}

View file

@ -54,7 +54,7 @@ fn expand_field(state: &Ident, idx: usize, field: &Field) -> TokenStream {
};
quote_spanned! {span=>
#[allow(clippy::clone_on_copy)]
#[allow(clippy::clone_on_copy, clippy::clone_on_ref_ptr)]
impl ::axum::extract::FromRef<#state> for #field_ty {
fn from_ref(state: &#state) -> Self {
#body

View file

@ -180,7 +180,7 @@ pub(crate) fn expand(item: syn::Item, tr: Trait) -> syn::Result<TokenStream> {
variants,
} = item;
let generics_error = format!("`#[derive({tr})] on enums don't support generics");
let generics_error = format!("`#[derive({tr})]` on enums don't support generics");
if !generics.params.is_empty() {
return Err(syn::Error::new_spanned(generics, generics_error));
@ -290,11 +290,7 @@ fn parse_single_generic_type_on_struct(
let field = fields_unnamed.unnamed.first().unwrap();
if let syn::Type::Path(type_path) = &field.ty {
if type_path
.path
.get_ident()
.map_or(true, |field_type_ident| field_type_ident != ty_ident)
{
if type_path.path.get_ident() != Some(ty_ident) {
return Err(syn::Error::new_spanned(
type_path,
format_args!(
@ -373,7 +369,6 @@ fn impl_struct_by_extracting_each_field(
Ok(match tr {
Trait::FromRequest => quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident
where
@ -390,7 +385,6 @@ fn impl_struct_by_extracting_each_field(
}
},
Trait::FromRequestParts => quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident
where
@ -435,7 +429,7 @@ fn extract_fields(
}
}
fn into_inner(via: Option<(attr::kw::via, syn::Path)>, ty_span: Span) -> TokenStream {
fn into_inner(via: &Option<(attr::kw::via, syn::Path)>, ty_span: Span) -> TokenStream {
if let Some((_, path)) = via {
let span = path.span();
quote_spanned! {span=>
@ -448,6 +442,23 @@ fn extract_fields(
}
}
fn into_outer(
via: &Option<(attr::kw::via, syn::Path)>,
ty_span: Span,
field_ty: &Type,
) -> TokenStream {
if let Some((_, path)) = via {
let span = path.span();
quote_spanned! {span=>
#path<#field_ty>
}
} else {
quote_spanned! {ty_span=>
#field_ty
}
}
}
let mut fields_iter = fields.iter();
let last = match tr {
@ -464,16 +475,17 @@ fn extract_fields(
let member = member(field, index);
let ty_span = field.ty.span();
let into_inner = into_inner(via, ty_span);
let into_inner = into_inner(&via, ty_span);
if peel_option(&field.ty).is_some() {
let field_ty = into_outer(&via, ty_span, peel_option(&field.ty).unwrap());
let tokens = match tr {
Trait::FromRequest => {
quote_spanned! {ty_span=>
#member: {
let (mut parts, body) = req.into_parts();
let value =
::axum::extract::FromRequestParts::from_request_parts(
<#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
&mut parts,
state,
)
@ -488,7 +500,7 @@ fn extract_fields(
Trait::FromRequestParts => {
quote_spanned! {ty_span=>
#member: {
::axum::extract::FromRequestParts::from_request_parts(
<#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
parts,
state,
)
@ -501,13 +513,14 @@ fn extract_fields(
};
Ok(tokens)
} else if peel_result_ok(&field.ty).is_some() {
let field_ty = into_outer(&via,ty_span, peel_result_ok(&field.ty).unwrap());
let tokens = match tr {
Trait::FromRequest => {
quote_spanned! {ty_span=>
#member: {
let (mut parts, body) = req.into_parts();
let value =
::axum::extract::FromRequestParts::from_request_parts(
<#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
&mut parts,
state,
)
@ -521,7 +534,7 @@ fn extract_fields(
Trait::FromRequestParts => {
quote_spanned! {ty_span=>
#member: {
::axum::extract::FromRequestParts::from_request_parts(
<#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
parts,
state,
)
@ -533,6 +546,7 @@ fn extract_fields(
};
Ok(tokens)
} else {
let field_ty = into_outer(&via,ty_span,&field.ty);
let map_err = if let Some(rejection) = rejection {
quote! { <#rejection as ::std::convert::From<_>>::from }
} else {
@ -545,7 +559,7 @@ fn extract_fields(
#member: {
let (mut parts, body) = req.into_parts();
let value =
::axum::extract::FromRequestParts::from_request_parts(
<#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
&mut parts,
state,
)
@ -560,7 +574,7 @@ fn extract_fields(
Trait::FromRequestParts => {
quote_spanned! {ty_span=>
#member: {
::axum::extract::FromRequestParts::from_request_parts(
<#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
parts,
state,
)
@ -582,26 +596,29 @@ fn extract_fields(
let member = member(field, fields.len() - 1);
let ty_span = field.ty.span();
let into_inner = into_inner(via, ty_span);
let into_inner = into_inner(&via, ty_span);
let item = if peel_option(&field.ty).is_some() {
let field_ty = into_outer(&via, ty_span, peel_option(&field.ty).unwrap());
quote_spanned! {ty_span=>
#member: {
::axum::extract::FromRequest::from_request(req, state)
<#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await
.ok()
.map(#into_inner)
},
}
} else if peel_result_ok(&field.ty).is_some() {
let field_ty = into_outer(&via, ty_span, peel_result_ok(&field.ty).unwrap());
quote_spanned! {ty_span=>
#member: {
::axum::extract::FromRequest::from_request(req, state)
<#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await
.map(#into_inner)
},
}
} else {
let field_ty = into_outer(&via, ty_span, &field.ty);
let map_err = if let Some(rejection) = rejection {
quote! { <#rejection as ::std::convert::From<_>>::from }
} else {
@ -610,7 +627,7 @@ fn extract_fields(
quote_spanned! {ty_span=>
#member: {
::axum::extract::FromRequest::from_request(req, state)
<#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await
.map(#into_inner)
.map_err(#map_err)?
@ -807,7 +824,6 @@ fn impl_struct_by_extracting_all_at_once(
let tokens = match tr {
Trait::FromRequest => {
quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident #ident_generics
where
@ -821,7 +837,7 @@ fn impl_struct_by_extracting_all_at_once(
req: ::axum::http::Request<::axum::body::Body>,
state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> {
::axum::extract::FromRequest::from_request(req, state)
<#via_path<#via_type_generics> as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await
.map(|#via_path(value)| #value_to_self)
.map_err(#map_err)
@ -831,7 +847,6 @@ fn impl_struct_by_extracting_all_at_once(
}
Trait::FromRequestParts => {
quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident #ident_generics
where
@ -845,7 +860,7 @@ fn impl_struct_by_extracting_all_at_once(
parts: &mut ::axum::http::request::Parts,
state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> {
::axum::extract::FromRequestParts::from_request_parts(parts, state)
<#via_path<#via_type_generics> as ::axum::extract::FromRequestParts<_>>::from_request_parts(parts, state)
.await
.map(|#via_path(value)| #value_to_self)
.map_err(#map_err)
@ -920,7 +935,6 @@ fn impl_enum_by_extracting_all_at_once(
let tokens = match tr {
Trait::FromRequest => {
quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident
where
@ -932,7 +946,7 @@ fn impl_enum_by_extracting_all_at_once(
req: ::axum::http::Request<::axum::body::Body>,
state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> {
::axum::extract::FromRequest::from_request(req, state)
<#path::<#ident> as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await
.map(|#path(inner)| inner)
.map_err(#map_err)
@ -942,7 +956,6 @@ fn impl_enum_by_extracting_all_at_once(
}
Trait::FromRequestParts => {
quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident
where
@ -954,7 +967,7 @@ fn impl_enum_by_extracting_all_at_once(
parts: &mut ::axum::http::request::Parts,
state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> {
::axum::extract::FromRequestParts::from_request_parts(parts, state)
<#path::<#ident> as ::axum::extract::FromRequestParts<_>>::from_request_parts(parts, state)
.await
.map(|#path(inner)| inner)
.map_err(#map_err)
@ -1003,7 +1016,7 @@ fn infer_state_type_from_field_attributes(fields: &Fields) -> impl Iterator<Item
match fields {
Fields::Named(fields_named) => {
Box::new(fields_named.named.iter().filter_map(|field| {
// TODO(david): its a little wasteful to parse the attributes again here
// TODO(david): it's a little wasteful to parse the attributes again here
// ideally we should parse things once and pass the data down
let FromRequestFieldAttrs { via } =
parse_attrs("from_request", &field.attrs).ok()?;
@ -1013,7 +1026,7 @@ fn infer_state_type_from_field_attributes(fields: &Fields) -> impl Iterator<Item
}
Fields::Unnamed(fields_unnamed) => {
Box::new(fields_unnamed.unnamed.iter().filter_map(|field| {
// TODO(david): its a little wasteful to parse the attributes again here
// TODO(david): it's a little wasteful to parse the attributes again here
// ideally we should parse things once and pass the data down
let FromRequestFieldAttrs { via } =
parse_attrs("from_request", &field.attrs).ok()?;

View file

@ -15,7 +15,6 @@
clippy::needless_borrow,
clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock,
clippy::match_on_vec_items,
clippy::imprecise_flops,
@ -44,6 +43,7 @@
#![cfg_attr(test, allow(clippy::float_cmp))]
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
use debug_handler::FunctionKind;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Parse, Type};
@ -233,6 +233,54 @@ use from_request::Trait::{FromRequest, FromRequestParts};
/// }
/// ```
///
/// ## Concrete state
///
/// If the extraction can be done only for a concrete state, that type can be specified with
/// `#[from_request(state(YourState))]`:
///
/// ```
/// use axum::extract::{FromRequest, FromRequestParts};
///
/// #[derive(Clone)]
/// struct CustomState;
///
/// struct MyInnerType;
///
/// impl FromRequestParts<CustomState> for MyInnerType {
/// // ...
/// # type Rejection = ();
///
/// # async fn from_request_parts(
/// # _parts: &mut axum::http::request::Parts,
/// # _state: &CustomState
/// # ) -> Result<Self, Self::Rejection> {
/// # todo!()
/// # }
/// }
///
/// #[derive(FromRequest)]
/// #[from_request(state(CustomState))]
/// struct MyExtractor {
/// custom: MyInnerType,
/// body: String,
/// }
/// ```
///
/// This is not needed for a `State<T>` as the type is inferred in that case.
///
/// ```
/// use axum::extract::{FromRequest, FromRequestParts, State};
///
/// #[derive(Clone)]
/// struct CustomState;
///
/// #[derive(FromRequest)]
/// struct MyExtractor {
/// custom: State<CustomState>,
/// body: String,
/// }
/// ```
///
/// # The whole type at once
///
/// By using `#[from_request(via(...))]` on the container you can extract the whole type at once,
@ -349,7 +397,7 @@ use from_request::Trait::{FromRequest, FromRequestParts};
///
/// # Known limitations
///
/// Generics are only supported on tuple structs with exactly on field. Thus this doesn't work
/// Generics are only supported on tuple structs with exactly one field. Thus this doesn't work
///
/// ```compile_fail
/// #[derive(axum_macros::FromRequest)]
@ -415,7 +463,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
expand_with(item, |item| from_request::expand(item, FromRequestParts))
}
/// Generates better error messages when applied handler functions.
/// Generates better error messages when applied to handler functions.
///
/// While using [`axum`], you can get long error messages for simple mistakes. For example:
///
@ -466,17 +514,15 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
///
/// As the error message says, handler function needs to be async.
///
/// ```
/// ```no_run
/// use axum::{routing::get, Router, debug_handler};
///
/// #[tokio::main]
/// async fn main() {
/// # async {
/// let app = Router::new().route("/", get(handler));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// # };
/// }
///
/// #[debug_handler]
@ -569,7 +615,65 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
return input;
#[cfg(debug_assertions)]
return expand_attr_with(_attr, input, debug_handler::expand);
return expand_attr_with(_attr, input, |attrs, item_fn| {
debug_handler::expand(attrs, item_fn, FunctionKind::Handler)
});
}
/// Generates better error messages when applied to middleware functions.
///
/// This works similarly to [`#[debug_handler]`](macro@debug_handler) except for middleware using
/// [`axum::middleware::from_fn`].
///
/// # Example
///
/// ```no_run
/// use axum::{
/// routing::get,
/// extract::Request,
/// response::Response,
/// Router,
/// middleware::{self, Next},
/// debug_middleware,
/// };
///
/// #[tokio::main]
/// async fn main() {
/// let app = Router::new()
/// .route("/", get(|| async {}))
/// .layer(middleware::from_fn(my_middleware));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// }
///
/// // if this wasn't a valid middleware function #[debug_middleware] would
/// // improve compile error
/// #[debug_middleware]
/// async fn my_middleware(
/// request: Request,
/// next: Next,
/// ) -> Response {
/// next.run(request).await
/// }
/// ```
///
/// # Performance
///
/// This macro has no effect when compiled with the release profile. (eg. `cargo build --release`)
///
/// [`axum`]: https://docs.rs/axum/latest
/// [`axum::middleware::from_fn`]: https://docs.rs/axum/0.7/axum/middleware/fn.from_fn.html
/// [`debug_middleware`]: macro@debug_middleware
#[proc_macro_attribute]
pub fn debug_middleware(_attr: TokenStream, input: TokenStream) -> TokenStream {
#[cfg(not(debug_assertions))]
return input;
#[cfg(debug_assertions)]
return expand_attr_with(_attr, input, |attrs, item_fn| {
debug_handler::expand(attrs, item_fn, FunctionKind::Middleware)
});
}
/// Private API: Do no use this!

View file

@ -133,7 +133,6 @@ fn expand_named_fields(
let map_err_rejection = map_err_rejection(&rejection);
let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident
where
@ -238,7 +237,6 @@ fn expand_unnamed_fields(
let map_err_rejection = map_err_rejection(&rejection);
let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident
where
@ -322,7 +320,6 @@ fn expand_unit_fields(
};
let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident
where
@ -386,8 +383,12 @@ fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
.split('/')
.map(|segment| {
if let Some(capture) = segment
.strip_prefix(':')
.or_else(|| segment.strip_prefix('*'))
.strip_prefix('{')
.and_then(|segment| segment.strip_suffix('}'))
.and_then(|segment| {
(!segment.starts_with('{') && !segment.ends_with('}')).then_some(segment)
})
.map(|capture| capture.strip_prefix('*').unwrap_or(capture))
{
Ok(Segment::Capture(capture.to_owned(), path.span()))
} else {

View file

@ -40,10 +40,10 @@ impl<I> WithPosition<I>
where
I: Iterator,
{
pub(crate) fn new(iter: I) -> WithPosition<I> {
pub(crate) fn new(iter: impl IntoIterator<IntoIter = I>) -> WithPosition<I> {
WithPosition {
handled_first: false,
peekable: iter.fuse().peekable(),
peekable: iter.into_iter().fuse().peekable(),
}
}
}

View file

@ -1,4 +1,3 @@
#![feature(diagnostic_namespace)]
use axum_macros::debug_handler;
#[debug_handler]

View file

@ -1,24 +1,24 @@
error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied
--> tests/debug_handler/fail/argument_not_extractor.rs:5:24
error[E0277]: the trait bound `bool: FromRequest<(), axum_core::extract::private::ViaParts>` is not satisfied
--> tests/debug_handler/fail/argument_not_extractor.rs:4:24
|
5 | async fn handler(_foo: bool) {}
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`
4 | async fn handler(_foo: bool) {}
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`, which is required by `bool: FromRequest<(), _>`
|
= note: Function argument is not a valid axum extractor.
See `https://docs.rs/axum/0.7/axum/extract/index.html` for details
= help: the following other types implement trait `FromRequestParts<S>`:
<HeaderMap as FromRequestParts<S>>
<Extension<T> as FromRequestParts<S>>
<Method as FromRequestParts<S>>
<axum::http::request::Parts as FromRequestParts<S>>
<Uri as FromRequestParts<S>>
<Version as FromRequestParts<S>>
<Extensions as FromRequestParts<S>>
<ConnectInfo<T> as FromRequestParts<S>>
`()` implements `FromRequestParts<S>`
`(T1, T2)` implements `FromRequestParts<S>`
`(T1, T2, T3)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
and $N others
= note: required for `bool` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
--> tests/debug_handler/fail/argument_not_extractor.rs:5:24
--> tests/debug_handler/fail/argument_not_extractor.rs:4:24
|
5 | async fn handler(_foo: bool) {}
4 | async fn handler(_foo: bool) {}
| ^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`

View file

@ -0,0 +1,9 @@
use axum::extract::Extension;
use axum_macros::debug_handler;
struct NonCloneType;
#[debug_handler]
async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
fn main() {}

View file

@ -0,0 +1,28 @@
error[E0277]: the trait bound `NonCloneType: Clone` is not satisfied
--> tests/debug_handler/fail/extension_not_clone.rs:7:38
|
7 | async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonCloneType`, which is required by `Extension<NonCloneType>: FromRequest<(), _>`
|
= help: the following other types implement trait `FromRequest<S, M>`:
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
(T1, T2, T3, T4, T5, T6, T7, T8, T9)
and $N others
= note: required for `Extension<NonCloneType>` to implement `FromRequestParts<()>`
= note: required for `Extension<NonCloneType>` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_test_extension_non_clone_0_from_request_check`
--> tests/debug_handler/fail/extension_not_clone.rs:7:38
|
7 | async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
| ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `__axum_macros_check_test_extension_non_clone_0_from_request_check`
help: consider annotating `NonCloneType` with `#[derive(Clone)]`
|
4 + #[derive(Clone)]
5 | struct NonCloneType;
|

View file

@ -1,12 +1,8 @@
use axum::{
async_trait,
extract::{Request, FromRequest},
};
use axum::extract::{FromRequest, Request};
use axum_macros::debug_handler;
struct A;
#[async_trait]
impl<S> FromRequest<S> for A
where
S: Send + Sync,

View file

@ -1,5 +1,5 @@
error: Handlers must only take owned values
--> tests/debug_handler/fail/extract_self_mut.rs:23:22
--> tests/debug_handler/fail/extract_self_mut.rs:19:22
|
23 | async fn handler(&mut self) {}
19 | async fn handler(&mut self) {}
| ^^^^^^^^^

View file

@ -1,12 +1,8 @@
use axum::{
async_trait,
extract::{Request, FromRequest},
};
use axum::extract::{FromRequest, Request};
use axum_macros::debug_handler;
struct A;
#[async_trait]
impl<S> FromRequest<S> for A
where
S: Send + Sync,

View file

@ -1,5 +1,5 @@
error: Handlers must only take owned values
--> tests/debug_handler/fail/extract_self_ref.rs:23:22
--> tests/debug_handler/fail/extract_self_ref.rs:19:22
|
23 | async fn handler(&self) {}
19 | async fn handler(&self) {}
| ^^^^^

View file

@ -4,6 +4,6 @@ use axum_macros::debug_handler;
struct Struct {}
#[debug_handler]
async fn handler(foo: Json<Struct>) {}
async fn handler(_foo: Json<Struct>) {}
fn main() {}

View file

@ -1,20 +1,51 @@
error[E0277]: the trait bound `for<'de> Struct: serde::de::Deserialize<'de>` is not satisfied
--> tests/debug_handler/fail/json_not_deserialize.rs:7:23
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
7 | async fn handler(foo: Json<Struct>) {}
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `Struct`
7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
|
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `Struct` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `serde::de::Deserialize<'de>`:
bool
char
isize
i8
i16
i32
i64
i128
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `Struct` to implement `serde::de::DeserializeOwned`
= note: required for `Json<Struct>` to implement `FromRequest<()>`
= help: see issue #48214
= help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
1 + #![feature(trivial_bounds)]
|
error[E0277]: the trait bound `for<'de> Struct: serde::de::Deserialize<'de>` is not satisfied
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
|
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `Struct` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `serde::de::Deserialize<'de>`:
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `Struct` to implement `serde::de::DeserializeOwned`
= note: required for `Json<Struct>` to implement `FromRequest<()>`
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`

View file

@ -1,5 +1,9 @@
use axum::{
body::Bytes,
http::{Method, Uri},
Json,
};
use axum_macros::debug_handler;
use axum::{Json, body::Bytes, http::{Method, Uri}};
#[debug_handler]
async fn one(_: Json<()>, _: String, _: Uri) {}

View file

@ -1,11 +1,11 @@
error: Can't have two extractors that consume the request body. `Json<_>` and `String` both do that.
--> tests/debug_handler/fail/multiple_request_consumers.rs:5:14
--> tests/debug_handler/fail/multiple_request_consumers.rs:9:14
|
5 | async fn one(_: Json<()>, _: String, _: Uri) {}
9 | async fn one(_: Json<()>, _: String, _: Uri) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: Can't have more than one extractor that consume the request body. `Json<_>`, `Bytes`, and `String` all do that.
--> tests/debug_handler/fail/multiple_request_consumers.rs:8:14
|
8 | async fn two(_: Json<()>, _: Method, _: Bytes, _: Uri, _: String) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
--> tests/debug_handler/fail/multiple_request_consumers.rs:12:14
|
12 | async fn two(_: Json<()>, _: Method, _: Bytes, _: Uri, _: String) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -4,7 +4,7 @@ error: future cannot be sent between threads safely
3 | #[debug_handler]
| ^^^^^^^^^^^^^^^^ future returned by `handler` is not `Send`
|
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`, which is required by `impl Future<Output = ()>: Send`
note: future is not `Send` as this value is used across an await
--> tests/debug_handler/fail/not_send.rs:6:14
|
@ -12,8 +12,6 @@ note: future is not `Send` as this value is used across an await
| --- has type `Rc<()>` which is not `Send`
6 | async {}.await;
| ^^^^^ await occurs here, with `_rc` maybe used later
7 | }
| - `_rc` is later dropped here
note: required by a bound in `check`
--> tests/debug_handler/fail/not_send.rs:3:1
|

View file

@ -0,0 +1,28 @@
use axum::response::AppendHeaders;
#[axum::debug_handler]
async fn handler() -> (
axum::http::StatusCode,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
axum::http::StatusCode,
) {
panic!()
}
fn main() {}

View file

@ -0,0 +1,12 @@
error: Cannot return tuples with more than 17 elements
--> tests/debug_handler/fail/output_tuple_too_many.rs:4:20
|
4 | async fn handler() -> (
| ____________________^
5 | | axum::http::StatusCode,
6 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
7 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
... |
23 | | axum::http::StatusCode,
24 | | ) {
| |_^

View file

@ -0,0 +1,9 @@
#[axum::debug_handler]
async fn handler() -> (
axum::http::request::Parts, // this should be response parts, not request parts
axum::http::StatusCode,
) {
panic!()
}
fn main() {}

View file

@ -0,0 +1,8 @@
error[E0308]: mismatched types
--> tests/debug_handler/fail/returning_request_parts.rs:3:5
|
3 | axum::http::request::Parts, // this should be response parts, not request parts
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| expected `axum::http::response::Parts`, found `axum::http::request::Parts`
| expected `axum::http::response::Parts` because of return type

View file

@ -0,0 +1,10 @@
#![allow(unused_parens)]
struct NotIntoResponse;
#[axum::debug_handler]
async fn handler() -> (NotIntoResponse) {
panic!()
}
fn main() {}

View file

@ -0,0 +1,21 @@
error[E0277]: the trait bound `NotIntoResponse: IntoResponse` is not satisfied
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
6 | async fn handler() -> (NotIntoResponse) {
| ^^^^^^^^^^^^^^^^^ the trait `IntoResponse` is not implemented for `NotIntoResponse`
|
= help: the following other types implement trait `IntoResponse`:
&'static [u8; N]
&'static [u8]
&'static str
()
(R,)
(Response<()>, R)
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
and $N others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
6 | async fn handler() -> (NotIntoResponse) {
| ^^^^^^^^^^^^^^^^^ required by this bound in `check`

View file

@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::http::Uri;
use axum_macros::debug_handler;
#[debug_handler]
async fn handler(
@ -20,6 +20,7 @@ async fn handler(
_e15: Uri,
_e16: Uri,
_e17: Uri,
) {}
) {
}
fn main() {}

View file

@ -1,5 +1,5 @@
use axum::{http::Uri, Json};
use axum_macros::debug_handler;
use axum::{Json, http::Uri};
#[debug_handler]
async fn one(_: Json<()>, _: Uri) {}

View file

@ -0,0 +1,27 @@
#![allow(unused_parens)]
#[axum::debug_handler]
async fn named_type() -> (
axum::http::StatusCode,
axum::Json<&'static str>,
axum::response::AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
) {
panic!()
}
struct CustomIntoResponse {}
impl axum::response::IntoResponse for CustomIntoResponse {
fn into_response(self) -> axum::response::Response {
todo!()
}
}
#[axum::debug_handler]
async fn custom_type() -> (
axum::http::StatusCode,
CustomIntoResponse,
axum::response::AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
) {
panic!()
}
fn main() {}

View file

@ -0,0 +1,49 @@
error: `Json<_>` must be the last element in a response tuple
--> tests/debug_handler/fail/wrong_return_tuple.rs:6:5
|
6 | axum::Json<&'static str>,
| ^^^^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not satisfied
--> tests/debug_handler/fail/wrong_return_tuple.rs:21:5
|
21 | CustomIntoResponse,
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
= help: the following other types implement trait `IntoResponseParts`:
()
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
and $N others
= help: see issue #48214
help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
3 + #![feature(trivial_bounds)]
|
error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not satisfied
--> tests/debug_handler/fail/wrong_return_tuple.rs:21:5
|
21 | CustomIntoResponse,
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
= help: the following other types implement trait `IntoResponseParts`:
()
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
and $N others
note: required by a bound in `__axum_macros_check_custom_type_into_response_parts_1_check`
--> tests/debug_handler/fail/wrong_return_tuple.rs:21:5
|
21 | CustomIntoResponse,
| ^^^^^^^^^^^^^^^^^^ required by this bound in `__axum_macros_check_custom_type_into_response_parts_1_check`

View file

@ -5,14 +5,14 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
| ^^^^ the trait `IntoResponse` is not implemented for `bool`
|
= help: the following other types implement trait `IntoResponse`:
Box<str>
Box<[u8]>
axum::body::Bytes
Body
axum::extract::rejection::FailedToBufferBody
axum::extract::rejection::LengthLimitError
axum::extract::rejection::UnknownBodyError
axum::extract::rejection::InvalidUtf8
&'static [u8; N]
&'static [u8]
&'static str
()
(R,)
(Response<()>, R)
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
and $N others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/wrong_return_type.rs:4:23

View file

@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::response::IntoResponse;
use axum_macros::debug_handler;
#[debug_handler]
async fn handler() -> impl IntoResponse {

View file

@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::extract::State;
use axum_macros::debug_handler;
#[debug_handler]
async fn handler(_: State<AppState>) {}
@ -8,22 +8,13 @@ async fn handler(_: State<AppState>) {}
async fn handler_2(_: axum::extract::State<AppState>) {}
#[debug_handler]
async fn handler_3(
_: axum::extract::State<AppState>,
_: axum::extract::State<AppState>,
) {}
async fn handler_3(_: axum::extract::State<AppState>, _: axum::extract::State<AppState>) {}
#[debug_handler]
async fn handler_4(
_: State<AppState>,
_: State<AppState>,
) {}
async fn handler_4(_: State<AppState>, _: State<AppState>) {}
#[debug_handler]
async fn handler_5(
_: axum::extract::State<AppState>,
_: State<AppState>,
) {}
async fn handler_5(_: axum::extract::State<AppState>, _: State<AppState>) {}
#[derive(Clone)]
struct AppState;

View file

@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::http::{Method, Uri};
use axum_macros::debug_handler;
#[debug_handler]
async fn handler(_one: Method, _two: Uri, _three: String) {}

View file

@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use std::future::{Ready, ready};
use std::future::{ready, Ready};
#[debug_handler]
fn handler() -> Ready<()> {

View file

@ -1,4 +1,4 @@
use axum::{async_trait, extract::FromRequestParts, http::request::Parts, response::IntoResponse};
use axum::{extract::FromRequestParts, http::request::Parts, response::IntoResponse};
use axum_macros::debug_handler;
fn main() {}
@ -115,7 +115,6 @@ impl A {
}
}
#[async_trait]
impl<S> FromRequestParts<S> for A
where
S: Send + Sync,

View file

@ -1,12 +1,8 @@
use axum::{
async_trait,
extract::{Request, FromRequest},
};
use axum::extract::{FromRequest, Request};
use axum_macros::debug_handler;
struct A;
#[async_trait]
impl<S> FromRequest<S> for A
where
S: Send + Sync,
@ -18,7 +14,6 @@ where
}
}
#[async_trait]
impl<S> FromRequest<S> for Box<A>
where
S: Send + Sync,

View file

@ -1,6 +1,5 @@
use axum::extract::{FromRef, FromRequest, Request};
use axum_macros::debug_handler;
use axum::extract::{Request, FromRef, FromRequest};
use axum::async_trait;
#[debug_handler(state = AppState)]
async fn handler(_: A) {}
@ -10,7 +9,6 @@ struct AppState;
struct A;
#[async_trait]
impl<S> FromRequest<S> for A
where
S: Send + Sync,

View file

@ -1,5 +1,5 @@
use axum::{extract::Request, extract::State};
use axum_macros::debug_handler;
use axum::{extract::State, extract::Request};
#[debug_handler(state = AppState)]
async fn handler(_: State<AppState>, _: Request) {}

View file

@ -0,0 +1,13 @@
use axum::{
debug_middleware,
extract::Request,
response::{IntoResponse, Response},
};
#[debug_middleware]
async fn my_middleware(request: Request) -> Response {
let _ = request;
().into_response()
}
fn main() {}

View file

@ -0,0 +1,7 @@
error: Middleware functions must take `axum::middleware::Next` as the last argument
--> tests/debug_middleware/fail/doesnt_take_next.rs:7:1
|
7 | #[debug_middleware]
| ^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the attribute macro `debug_middleware` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,8 @@
use axum::{debug_middleware, extract::Request, middleware::Next, response::Response};
#[debug_middleware]
async fn my_middleware(next: Next, request: Request) -> Response {
next.run(request).await
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: `axum::middleware::Next` must the last argument
--> tests/debug_middleware/fail/next_not_last.rs:4:24
|
4 | async fn my_middleware(next: Next, request: Request) -> Response {
| ^^^^^^^^^^

Some files were not shown because too many files have changed in this diff Show more