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
330 changed files with 6760 additions and 2682 deletions
+72 -33
View File
@@ -2,7 +2,7 @@ name: CI
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
MSRV: '1.66' MSRV: '1.75'
on: on:
push: push:
@@ -12,37 +12,46 @@ on:
jobs: jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: taiki-e/install-action@protoc - uses: taiki-e/install-action@protoc
- uses: dtolnay/rust-toolchain@beta - uses: dtolnay/rust-toolchain@beta
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Check - name: Check
run: cargo clippy --workspace --all-targets --all-features -- -D warnings run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: rustfmt - name: rustfmt
run: cargo fmt --all --check run: cargo fmt --all --check
check-docs: check-docs:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: cargo doc - name: cargo doc
env: env:
RUSTDOCFLAGS: "-D rustdoc::all -A rustdoc::private-doc-tests" RUSTDOCFLAGS: "-D rustdoc::all -A rustdoc::private-doc-tests"
run: cargo doc --all-features --no-deps run: cargo doc --all-features --no-deps
cargo-hack: cargo-hack:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: taiki-e/install-action@protoc - uses: taiki-e/install-action@protoc
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Install cargo-hack - name: Install cargo-hack
run: | 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 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 run: cargo hack check --each-feature --no-dev-deps --all
cargo-public-api-crates: cargo-public-api-crates:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
crate: [axum, axum-core, axum-extra, axum-macros] crate: [axum, axum-core, axum-extra, axum-macros]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly # 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 - 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 - name: Install cargo-public-api-crates
run: | run: |
cargo install --git https://github.com/davidpdrsn/cargo-public-api-crates 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 - 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: test-versions:
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
rust: [stable, beta] rust: [stable, beta]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: taiki-e/install-action@protoc - uses: taiki-e/install-action@protoc
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: ${{ matrix.rust }} toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Run tests - name: Run tests
run: cargo test --workspace --all-features --all-targets 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: test-nightly:
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Get rust-toolchain version - name: Get rust-toolchain version
id: rust-toolchain id: rust-toolchain
run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT
@@ -93,23 +114,29 @@ jobs:
with: with:
toolchain: ${{ steps.rust-toolchain.outputs.version }} toolchain: ${{ steps.rust-toolchain.outputs.version }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Run nightly tests - name: Run nightly tests
working-directory: axum-macros working-directory: axum-macros
run: cargo test 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 # so we only test axum itself on our MSRV
test-msrv: test-msrv:
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: ${{ env.MSRV }} toolchain: ${{ env.MSRV }}
- name: "install Rust nightly" - name: "install Rust nightly"
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Select minimal version - name: Select minimal version
run: cargo +nightly update -Z minimal-versions run: cargo +nightly update -Z minimal-versions
- name: Fix up Cargo.lock - name: Fix up Cargo.lock
@@ -137,17 +164,20 @@ jobs:
test-docs: test-docs:
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Run doc tests - name: Run doc tests
run: cargo test --all-features --doc run: cargo test --all-features --doc
deny-check: deny-check:
name: cargo-deny check name: cargo-deny check
runs-on: ubuntu-latest runs-on: ubuntu-24.04
continue-on-error: ${{ matrix.checks == 'advisories' }} continue-on-error: ${{ matrix.checks == 'advisories' }}
strategy: strategy:
matrix: matrix:
@@ -155,21 +185,24 @@ jobs:
- advisories - advisories
- bans licenses sources - bans licenses sources
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
with: with:
command: check ${{ matrix.checks }} command: check ${{ matrix.checks }}
arguments: --all-features --manifest-path axum/Cargo.toml manifest-path: axum/Cargo.toml
armv5te-unknown-linux-musleabi: armv5te-unknown-linux-musleabi:
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
target: armv5te-unknown-linux-musleabi target: armv5te-unknown-linux-musleabi
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Check - name: Check
env: env:
# Clang has native cross-compilation support # Clang has native cross-compilation support
@@ -187,13 +220,16 @@ jobs:
wasm32-unknown-unknown: wasm32-unknown-unknown:
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
target: wasm32-unknown-unknown target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Check - name: Check
run: > run: >
cargo cargo
@@ -202,11 +238,14 @@ jobs:
--target wasm32-unknown-unknown --target wasm32-unknown-unknown
dependencies-are-sorted: dependencies-are-sorted:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@beta - uses: dtolnay/rust-toolchain@beta
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
prefix-key: "v0-rust-ubuntu-24.04"
- name: Install cargo-sort - name: Install cargo-sort
run: | run: |
cargo install cargo-sort cargo install cargo-sort
@@ -219,12 +258,12 @@ jobs:
typos: typos:
name: Spell Check with 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 if: github.event_name == 'push' || !github.event.pull_request.draft
steps: steps:
- name: Checkout Actions Repository - name: Checkout Actions Repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Check the spelling of the files in our repo - name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.16.2 uses: crate-ci/typos@v1.20.8
+1 -1
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 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 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 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. 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 [hiding-a-comment]: https://help.github.com/articles/managing-disruptive-comments/#hiding-a-comment
+3
View File
@@ -5,3 +5,6 @@ default-members = ["axum", "axum-*"]
# Example has been deleted, but README.md remains # Example has been deleted, but README.md remains
exclude = ["examples/async-graphql"] exclude = ["examples/async-graphql"]
resolver = "2" resolver = "2"
[workspace.package]
rust-version = "1.75"
+11 -1
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-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 - [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). - [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-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-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-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. - [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. - [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. - [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. - [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 ## 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]. - [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. 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 - [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 - [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 - [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! - [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. - [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. - [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. - [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 - [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 - [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) - [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. - [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 [Realworld]: https://github.com/gothinkster/realworld
[SQLx]: https://github.com/launchbadge/sqlx [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 - [Introduction to axum]: YouTube playlist
- [Rust Axum Full Course]: YouTube video - [Rust Axum Full Course]: YouTube video
- [Deploying Axum projects with Shuttle] - [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]: https://github.com/programatik29/axum-tutorial
[axum-tutorial-website]: https://programatik29.github.io/axum-tutorial/ [axum-tutorial-website]: https://programatik29.github.io/axum-tutorial/
+37 -2
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/), 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). 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) # 0.4.2 (29. December, 2023)
+11 -12
View File
@@ -2,14 +2,14 @@
categories = ["asynchronous", "network-programming", "web-programming"] categories = ["asynchronous", "network-programming", "web-programming"]
description = "Core types and traits for axum" description = "Core types and traits for axum"
edition = "2021" edition = "2021"
rust-version = "1.56" rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum" homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"] keywords = ["http", "web", "framework"]
license = "MIT" license = "MIT"
name = "axum-core" name = "axum-core"
readme = "README.md" readme = "README.md"
repository = "https://github.com/tokio-rs/axum" 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] [features]
tracing = ["dep:tracing"] tracing = ["dep:tracing"]
@@ -18,32 +18,29 @@ tracing = ["dep:tracing"]
__private_docs = ["dep:tower-http"] __private_docs = ["dep:tower-http"]
[dependencies] [dependencies]
async-trait = "0.1.67" bytes = "1.2"
bytes = "1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] } futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0" http = "1.0.0"
http-body = "1.0.0" http-body = "1.0.0"
http-body-util = "0.1.0" http-body-util = "0.1.0"
mime = "0.3.16" mime = "0.3.16"
pin-project-lite = "0.2.7" 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-layer = "0.3"
tower-service = "0.3" tower-service = "0.3"
# optional dependencies # 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 } tracing = { version = "0.1.37", default-features = false, optional = true }
[build-dependencies]
rustversion = "1.0.9"
[dev-dependencies] [dev-dependencies]
axum = { path = "../axum", version = "0.7.2" } axum = { path = "../axum" }
axum-extra = { path = "../axum-extra", features = ["typed-header"] } axum-extra = { path = "../axum-extra", features = ["typed-header"] }
futures-util = { version = "0.3", default-features = false, features = ["alloc"] } futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
hyper = "1.0.0" hyper = "1.0.0"
tokio = { version = "1.25.0", features = ["macros"] } 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] [package.metadata.cargo-public-api-crates]
allowed = [ allowed = [
@@ -57,6 +54,8 @@ allowed = [
"http_body", "http_body",
] ]
[package.metadata.cargo-machete]
ignored = ["tower-http"] # See __private_docs feature
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"]
+1 -1
View File
@@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version ## Minimum supported Rust version
axum-core's MSRV is 1.56. axum-core's MSRV is 1.75.
## Getting Help ## Getting Help
-7
View File
@@ -1,7 +0,0 @@
#[rustversion::nightly]
fn main() {
println!("cargo:rustc-cfg=nightly_error_messages");
}
#[rustversion::not(nightly)]
fn main() {}
+1 -3
View File
@@ -6,13 +6,11 @@ mod tests {
use std::convert::Infallible; use std::convert::Infallible;
use crate::extract::{FromRef, FromRequestParts}; use crate::extract::{FromRef, FromRequestParts};
use async_trait::async_trait;
use http::request::Parts; use http::request::Parts;
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
pub(crate) struct State<S>(pub(crate) S); pub(crate) struct State<S>(pub(crate) S);
#[async_trait]
impl<OuterState, InnerState> FromRequestParts<OuterState> for State<InnerState> impl<OuterState, InnerState> FromRequestParts<OuterState> for State<InnerState>
where where
InnerState: FromRef<OuterState>, InnerState: FromRef<OuterState>,
@@ -30,9 +28,9 @@ mod tests {
} }
// some extractor that requires the state, such as `SignedCookieJar` // some extractor that requires the state, such as `SignedCookieJar`
#[allow(dead_code)]
pub(crate) struct RequiresState(pub(crate) String); pub(crate) struct RequiresState(pub(crate) String);
#[async_trait]
impl<S> FromRequestParts<S> for RequiresState impl<S> FromRequestParts<S> for RequiresState
where where
S: Send + Sync, S: Send + Sync,
+23 -31
View File
@@ -1,6 +1,6 @@
use crate::body::Body; use crate::body::Body;
use crate::extract::{DefaultBodyLimitKind, FromRequest, FromRequestParts, Request}; use crate::extract::{DefaultBodyLimitKind, FromRequest, FromRequestParts, Request};
use futures_util::future::BoxFuture; use std::future::Future;
mod sealed { mod sealed {
pub trait Sealed {} pub trait Sealed {}
@@ -20,7 +20,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// ///
/// ``` /// ```
/// use axum::{ /// use axum::{
/// async_trait,
/// extract::{Request, FromRequest}, /// extract::{Request, FromRequest},
/// body::Body, /// body::Body,
/// http::{header::CONTENT_TYPE, StatusCode}, /// http::{header::CONTENT_TYPE, StatusCode},
@@ -30,7 +29,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// ///
/// struct FormOrJson<T>(T); /// struct FormOrJson<T>(T);
/// ///
/// #[async_trait]
/// impl<S, T> FromRequest<S> for FormOrJson<T> /// impl<S, T> FromRequest<S> for FormOrJson<T>
/// where /// where
/// Json<T>: FromRequest<()>, /// 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 where
E: FromRequest<(), M> + 'static, E: FromRequest<(), M> + 'static,
M: 'static; M: 'static;
@@ -83,7 +81,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// ///
/// ``` /// ```
/// use axum::{ /// use axum::{
/// async_trait,
/// body::Body, /// body::Body,
/// extract::{Request, FromRef, FromRequest}, /// extract::{Request, FromRef, FromRequest},
/// RequestExt, /// RequestExt,
@@ -93,7 +90,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// requires_state: RequiresState, /// requires_state: RequiresState,
/// } /// }
/// ///
/// #[async_trait]
/// impl<S> FromRequest<S> for MyExtractor /// impl<S> FromRequest<S> for MyExtractor
/// where /// where
/// String: FromRef<S>, /// String: FromRef<S>,
@@ -111,7 +107,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// // some extractor that consumes the request body and requires state /// // some extractor that consumes the request body and requires state
/// struct RequiresState { /* ... */ } /// struct RequiresState { /* ... */ }
/// ///
/// #[async_trait]
/// impl<S> FromRequest<S> for RequiresState /// impl<S> FromRequest<S> for RequiresState
/// where /// where
/// String: FromRef<S>, /// 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 where
E: FromRequest<S, M> + 'static, E: FromRequest<S, M> + 'static,
S: Send + Sync; S: Send + Sync;
@@ -137,7 +135,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// ///
/// ``` /// ```
/// use axum::{ /// use axum::{
/// async_trait,
/// extract::{Path, Request, FromRequest}, /// extract::{Path, Request, FromRequest},
/// response::{IntoResponse, Response}, /// response::{IntoResponse, Response},
/// body::Body, /// body::Body,
@@ -154,7 +151,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// payload: T, /// payload: T,
/// } /// }
/// ///
/// #[async_trait]
/// impl<S, T> FromRequest<S> for MyExtractor<T> /// impl<S, T> FromRequest<S> for MyExtractor<T>
/// where /// where
/// S: Send + Sync, /// 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 where
E: FromRequestParts<()> + 'static; E: FromRequestParts<()> + 'static;
@@ -191,7 +187,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// ///
/// ``` /// ```
/// use axum::{ /// use axum::{
/// async_trait,
/// extract::{Request, FromRef, FromRequest, FromRequestParts}, /// extract::{Request, FromRef, FromRequest, FromRequestParts},
/// http::request::Parts, /// http::request::Parts,
/// response::{IntoResponse, Response}, /// response::{IntoResponse, Response},
@@ -204,7 +199,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// payload: T, /// payload: T,
/// } /// }
/// ///
/// #[async_trait]
/// impl<S, T> FromRequest<S> for MyExtractor<T> /// impl<S, T> FromRequest<S> for MyExtractor<T>
/// where /// where
/// String: FromRef<S>, /// String: FromRef<S>,
@@ -234,7 +228,6 @@ pub trait RequestExt: sealed::Sealed + Sized {
/// ///
/// struct RequiresState {} /// struct RequiresState {}
/// ///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for RequiresState /// impl<S> FromRequestParts<S> for RequiresState
/// where /// where
/// String: FromRef<S>, /// String: FromRef<S>,
@@ -250,7 +243,7 @@ pub trait RequestExt: sealed::Sealed + Sized {
fn extract_parts_with_state<'a, E, S>( fn extract_parts_with_state<'a, E, S>(
&'a mut self, &'a mut self,
state: &'a S, state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>> ) -> impl Future<Output = Result<E, E::Rejection>> + Send + 'a
where where
E: FromRequestParts<S> + 'static, E: FromRequestParts<S> + 'static,
S: Send + Sync; S: Send + Sync;
@@ -267,7 +260,7 @@ pub trait RequestExt: sealed::Sealed + Sized {
} }
impl RequestExt for Request { 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 where
E: FromRequest<(), M> + 'static, E: FromRequest<(), M> + 'static,
M: 'static, M: 'static,
@@ -275,7 +268,10 @@ impl RequestExt for Request {
self.extract_with_state(&()) 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 where
E: FromRequest<S, M> + 'static, E: FromRequest<S, M> + 'static,
S: Send + Sync, S: Send + Sync,
@@ -283,17 +279,17 @@ impl RequestExt for Request {
E::from_request(self, state) 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 where
E: FromRequestParts<()> + 'static, E: FromRequestParts<()> + 'static,
{ {
self.extract_parts_with_state(&()) 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, &'a mut self,
state: &'a S, state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>> ) -> Result<E, E::Rejection>
where where
E: FromRequestParts<S> + 'static, E: FromRequestParts<S> + 'static,
S: Send + Sync, S: Send + Sync,
@@ -306,17 +302,15 @@ impl RequestExt for Request {
*req.extensions_mut() = std::mem::take(self.extensions_mut()); *req.extensions_mut() = std::mem::take(self.extensions_mut());
let (mut parts, ()) = req.into_parts(); 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.version_mut() = parts.version;
*self.method_mut() = parts.method.clone(); *self.method_mut() = parts.method.clone();
*self.uri_mut() = parts.uri.clone(); *self.uri_mut() = parts.uri.clone();
*self.headers_mut() = std::mem::take(&mut parts.headers); *self.headers_mut() = std::mem::take(&mut parts.headers);
*self.extensions_mut() = std::mem::take(&mut parts.extensions); *self.extensions_mut() = std::mem::take(&mut parts.extensions);
result result
})
} }
fn with_limited_body(self) -> Request { fn with_limited_body(self) -> Request {
@@ -345,7 +339,6 @@ mod tests {
ext_traits::tests::{RequiresState, State}, ext_traits::tests::{RequiresState, State},
extract::FromRef, extract::FromRef,
}; };
use async_trait::async_trait;
use http::Method; use http::Method;
#[tokio::test] #[tokio::test]
@@ -414,7 +407,6 @@ mod tests {
body: String, body: String,
} }
#[async_trait]
impl<S> FromRequest<S> for WorksForCustomExtractor impl<S> FromRequest<S> for WorksForCustomExtractor
where where
S: Send + Sync, S: Send + Sync,
+5 -12
View File
@@ -1,6 +1,6 @@
use crate::extract::FromRequestParts; use crate::extract::FromRequestParts;
use futures_util::future::BoxFuture;
use http::request::Parts; use http::request::Parts;
use std::future::Future;
mod sealed { mod sealed {
pub trait Sealed {} pub trait Sealed {}
@@ -21,7 +21,6 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// response::{Response, IntoResponse}, /// response::{Response, IntoResponse},
/// http::request::Parts, /// http::request::Parts,
/// RequestPartsExt, /// RequestPartsExt,
/// async_trait,
/// }; /// };
/// use std::collections::HashMap; /// use std::collections::HashMap;
/// ///
@@ -30,7 +29,6 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// query_params: HashMap<String, String>, /// query_params: HashMap<String, String>,
/// } /// }
/// ///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for MyExtractor /// impl<S> FromRequestParts<S> for MyExtractor
/// where /// where
/// S: Send + Sync, /// 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 where
E: FromRequestParts<()> + 'static; E: FromRequestParts<()> + 'static;
@@ -70,14 +68,12 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// response::{Response, IntoResponse}, /// response::{Response, IntoResponse},
/// http::request::Parts, /// http::request::Parts,
/// RequestPartsExt, /// RequestPartsExt,
/// async_trait,
/// }; /// };
/// ///
/// struct MyExtractor { /// struct MyExtractor {
/// requires_state: RequiresState, /// requires_state: RequiresState,
/// } /// }
/// ///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for MyExtractor /// impl<S> FromRequestParts<S> for MyExtractor
/// where /// where
/// String: FromRef<S>, /// String: FromRef<S>,
@@ -97,7 +93,6 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
/// struct RequiresState { /* ... */ } /// struct RequiresState { /* ... */ }
/// ///
/// // some extractor that requires a `String` in the state /// // some extractor that requires a `String` in the state
/// #[async_trait]
/// impl<S> FromRequestParts<S> for RequiresState /// impl<S> FromRequestParts<S> for RequiresState
/// where /// where
/// String: FromRef<S>, /// String: FromRef<S>,
@@ -113,14 +108,14 @@ pub trait RequestPartsExt: sealed::Sealed + Sized {
fn extract_with_state<'a, E, S>( fn extract_with_state<'a, E, S>(
&'a mut self, &'a mut self,
state: &'a S, state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>> ) -> impl Future<Output = Result<E, E::Rejection>> + Send + 'a
where where
E: FromRequestParts<S> + 'static, E: FromRequestParts<S> + 'static,
S: Send + Sync; S: Send + Sync;
} }
impl RequestPartsExt for Parts { 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 where
E: FromRequestParts<()> + 'static, E: FromRequestParts<()> + 'static,
{ {
@@ -130,7 +125,7 @@ impl RequestPartsExt for Parts {
fn extract_with_state<'a, E, S>( fn extract_with_state<'a, E, S>(
&'a mut self, &'a mut self,
state: &'a S, state: &'a S,
) -> BoxFuture<'a, Result<E, E::Rejection>> ) -> impl Future<Output = Result<E, E::Rejection>> + Send + 'a
where where
E: FromRequestParts<S> + 'static, E: FromRequestParts<S> + 'static,
S: Send + Sync, S: Send + Sync,
@@ -148,7 +143,6 @@ mod tests {
ext_traits::tests::{RequiresState, State}, ext_traits::tests::{RequiresState, State},
extract::FromRef, extract::FromRef,
}; };
use async_trait::async_trait;
use http::{Method, Request}; use http::{Method, Request};
#[tokio::test] #[tokio::test]
@@ -181,7 +175,6 @@ mod tests {
from_state: String, from_state: String,
} }
#[async_trait]
impl<S> FromRequestParts<S> for WorksForCustomExtractor impl<S> FromRequestParts<S> for WorksForCustomExtractor
where where
S: Send + Sync, S: Send + Sync,
+3 -3
View File
@@ -72,7 +72,7 @@ use tower_layer::Layer;
/// [`RequestBodyLimit`]: tower_http::limit::RequestBodyLimit /// [`RequestBodyLimit`]: tower_http::limit::RequestBodyLimit
/// [`RequestExt::with_limited_body`]: crate::RequestExt::with_limited_body /// [`RequestExt::with_limited_body`]: crate::RequestExt::with_limited_body
/// [`RequestExt::into_limited_body`]: crate::RequestExt::into_limited_body /// [`RequestExt::into_limited_body`]: crate::RequestExt::into_limited_body
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy)]
#[must_use] #[must_use]
pub struct DefaultBodyLimit { pub struct DefaultBodyLimit {
kind: DefaultBodyLimitKind, kind: DefaultBodyLimitKind,
@@ -116,7 +116,7 @@ impl DefaultBodyLimit {
/// [`Bytes`]: bytes::Bytes /// [`Bytes`]: bytes::Bytes
/// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html /// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html
/// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.html /// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.html
pub fn disable() -> Self { pub const fn disable() -> Self {
Self { Self {
kind: DefaultBodyLimitKind::Disable, kind: DefaultBodyLimitKind::Disable,
} }
@@ -149,7 +149,7 @@ impl DefaultBodyLimit {
/// [`Bytes::from_request`]: bytes::Bytes /// [`Bytes::from_request`]: bytes::Bytes
/// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html /// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html
/// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.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 { Self {
kind: DefaultBodyLimitKind::Limit(limit), kind: DefaultBodyLimitKind::Limit(limit),
} }
+13 -14
View File
@@ -5,9 +5,9 @@
//! [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html //! [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
use crate::{body::Body, response::IntoResponse}; use crate::{body::Body, response::IntoResponse};
use async_trait::async_trait;
use http::request::Parts; use http::request::Parts;
use std::convert::Infallible; use std::convert::Infallible;
use std::future::Future;
pub mod rejection; pub mod rejection;
@@ -42,9 +42,8 @@ mod private {
/// See [`axum::extract`] for more general docs about extractors. /// See [`axum::extract`] for more general docs about extractors.
/// ///
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html /// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
#[async_trait] #[rustversion::attr(
#[cfg_attr( since(1.78),
nightly_error_messages,
diagnostic::on_unimplemented( 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", 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; type Rejection: IntoResponse;
/// Perform the extraction. /// 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. /// 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. /// See [`axum::extract`] for more general docs about extractors.
/// ///
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html /// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
#[async_trait] #[rustversion::attr(
#[cfg_attr( since(1.78),
nightly_error_messages,
diagnostic::on_unimplemented( 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", 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; type Rejection: IntoResponse;
/// Perform the extraction. /// 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 impl<S, T> FromRequest<S, private::ViaParts> for T
where where
S: Send + Sync, S: Send + Sync,
@@ -99,7 +102,6 @@ where
} }
} }
#[async_trait]
impl<S, T> FromRequestParts<S> for Option<T> impl<S, T> FromRequestParts<S> for Option<T>
where where
T: FromRequestParts<S>, T: FromRequestParts<S>,
@@ -115,7 +117,6 @@ where
} }
} }
#[async_trait]
impl<S, T> FromRequest<S> for Option<T> impl<S, T> FromRequest<S> for Option<T>
where where
T: FromRequest<S>, T: FromRequest<S>,
@@ -128,7 +129,6 @@ where
} }
} }
#[async_trait]
impl<S, T> FromRequestParts<S> for Result<T, T::Rejection> impl<S, T> FromRequestParts<S> for Result<T, T::Rejection>
where where
T: FromRequestParts<S>, T: FromRequestParts<S>,
@@ -141,7 +141,6 @@ where
} }
} }
#[async_trait]
impl<S, T> FromRequest<S> for Result<T, T::Rejection> impl<S, T> FromRequest<S> for Result<T, T::Rejection>
where where
T: FromRequest<S>, T: FromRequest<S>,
+1 -1
View File
@@ -42,7 +42,7 @@ define_rejection! {
#[body = "Failed to buffer the request body"] #[body = "Failed to buffer the request body"]
/// Encountered some other error when buffering the 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`]. /// otherwise wrapping request bodies in [`http_body_util::Limited`].
pub struct LengthLimitError(Error); pub struct LengthLimitError(Error);
} }
+32 -15
View File
@@ -1,12 +1,10 @@
use super::{rejection::*, FromRequest, FromRequestParts, Request}; use super::{rejection::*, FromRequest, FromRequestParts, Request};
use crate::{body::Body, RequestExt}; use crate::{body::Body, RequestExt};
use async_trait::async_trait; use bytes::{BufMut, Bytes, BytesMut};
use bytes::Bytes;
use http::{request::Parts, Extensions, HeaderMap, Method, Uri, Version}; use http::{request::Parts, Extensions, HeaderMap, Method, Uri, Version};
use http_body_util::BodyExt; use http_body_util::BodyExt;
use std::convert::Infallible; use std::convert::Infallible;
#[async_trait]
impl<S> FromRequest<S> for Request impl<S> FromRequest<S> for Request
where where
S: Send + Sync, S: Send + Sync,
@@ -18,7 +16,6 @@ where
} }
} }
#[async_trait]
impl<S> FromRequestParts<S> for Method impl<S> FromRequestParts<S> for Method
where where
S: Send + Sync, S: Send + Sync,
@@ -30,7 +27,6 @@ where
} }
} }
#[async_trait]
impl<S> FromRequestParts<S> for Uri impl<S> FromRequestParts<S> for Uri
where where
S: Send + Sync, S: Send + Sync,
@@ -42,7 +38,6 @@ where
} }
} }
#[async_trait]
impl<S> FromRequestParts<S> for Version impl<S> FromRequestParts<S> for Version
where where
S: Send + Sync, S: Send + Sync,
@@ -59,7 +54,6 @@ where
/// Prefer using [`TypedHeader`] to extract only the headers you need. /// Prefer using [`TypedHeader`] to extract only the headers you need.
/// ///
/// [`TypedHeader`]: https://docs.rs/axum/0.7/axum/extract/struct.TypedHeader.html /// [`TypedHeader`]: https://docs.rs/axum/0.7/axum/extract/struct.TypedHeader.html
#[async_trait]
impl<S> FromRequestParts<S> for HeaderMap impl<S> FromRequestParts<S> for HeaderMap
where where
S: Send + Sync, 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 impl<S> FromRequest<S> for Bytes
where where
S: Send + Sync, S: Send + Sync,
@@ -90,7 +113,6 @@ where
} }
} }
#[async_trait]
impl<S> FromRequest<S> for String impl<S> FromRequest<S> for String
where where
S: Send + Sync, S: Send + Sync,
@@ -106,15 +128,12 @@ where
} }
})?; })?;
let string = std::str::from_utf8(&bytes) let string = String::from_utf8(bytes.into()).map_err(InvalidUtf8::from_err)?;
.map_err(InvalidUtf8::from_err)?
.to_owned();
Ok(string) Ok(string)
} }
} }
#[async_trait]
impl<S> FromRequestParts<S> for Parts impl<S> FromRequestParts<S> for Parts
where where
S: Send + Sync, S: Send + Sync,
@@ -126,7 +145,6 @@ where
} }
} }
#[async_trait]
impl<S> FromRequestParts<S> for Extensions impl<S> FromRequestParts<S> for Extensions
where where
S: Send + Sync, S: Send + Sync,
@@ -138,7 +156,6 @@ where
} }
} }
#[async_trait]
impl<S> FromRequest<S> for Body impl<S> FromRequest<S> for Body
where where
S: Send + Sync, S: Send + Sync,
-4
View File
@@ -1,10 +1,8 @@
use super::{FromRequest, FromRequestParts, Request}; use super::{FromRequest, FromRequestParts, Request};
use crate::response::{IntoResponse, Response}; use crate::response::{IntoResponse, Response};
use async_trait::async_trait;
use http::request::Parts; use http::request::Parts;
use std::convert::Infallible; use std::convert::Infallible;
#[async_trait]
impl<S> FromRequestParts<S> for () impl<S> FromRequestParts<S> for ()
where where
S: Send + Sync, S: Send + Sync,
@@ -20,7 +18,6 @@ macro_rules! impl_from_request {
( (
[$($ty:ident),*], $last:ident [$($ty:ident),*], $last:ident
) => { ) => {
#[async_trait]
#[allow(non_snake_case, unused_mut, unused_variables)] #[allow(non_snake_case, unused_mut, unused_variables)]
impl<S, $($ty,)* $last> FromRequestParts<S> for ($($ty,)* $last,) impl<S, $($ty,)* $last> FromRequestParts<S> for ($($ty,)* $last,)
where 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 // This impl must not be generic over M, otherwise it would conflict with the blanket
// implementation of `FromRequest<S, Mut>` for `T: FromRequestParts<S>`. // implementation of `FromRequest<S, Mut>` for `T: FromRequestParts<S>`.
#[async_trait]
#[allow(non_snake_case, unused_mut, unused_variables)] #[allow(non_snake_case, unused_mut, unused_variables)]
impl<S, $($ty,)* $last> FromRequest<S> for ($($ty,)* $last,) impl<S, $($ty,)* $last> FromRequest<S> for ($($ty,)* $last,)
where where
+5 -2
View File
@@ -1,4 +1,3 @@
#![cfg_attr(nightly_error_messages, feature(diagnostic_namespace))]
//! Core types and traits for [`axum`]. //! Core types and traits for [`axum`].
//! //!
//! Libraries authors that want to provide [`FromRequest`] or [`IntoResponse`] implementations //! Libraries authors that want to provide [`FromRequest`] or [`IntoResponse`] implementations
@@ -22,7 +21,6 @@
clippy::needless_borrow, clippy::needless_borrow,
clippy::match_wildcard_for_single_variants, clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex, clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock, clippy::await_holding_lock,
clippy::match_on_vec_items, clippy::match_on_vec_items,
clippy::imprecise_flops, clippy::imprecise_flops,
@@ -52,6 +50,11 @@
#[macro_use] #[macro_use]
pub(crate) mod macros; pub(crate) mod macros;
#[doc(hidden)] // macro helpers
pub mod __private {
#[cfg(feature = "tracing")]
pub use tracing;
}
mod error; mod error;
mod ext_traits; mod ext_traits;
+15 -6
View File
@@ -1,4 +1,5 @@
/// Private API. /// Private API.
#[cfg(feature = "tracing")]
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! __log_rejection { macro_rules! __log_rejection {
@@ -7,20 +8,30 @@ macro_rules! __log_rejection {
body_text = $body_text:expr, body_text = $body_text:expr,
status = $status:expr, status = $status:expr,
) => { ) => {
#[cfg(feature = "tracing")]
{ {
tracing::event!( $crate::__private::tracing::event!(
target: "axum::rejection", target: "axum::rejection",
tracing::Level::TRACE, $crate::__private::tracing::Level::TRACE,
status = $status.as_u16(), status = $status.as_u16(),
body = $body_text, body = $body_text,
rejection_type = std::any::type_name::<$ty>(), rejection_type = ::std::any::type_name::<$ty>(),
"rejecting request", "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. /// Private API.
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
@@ -303,8 +314,6 @@ mod composite_rejection_tests {
#[allow(dead_code, unreachable_pub)] #[allow(dead_code, unreachable_pub)]
mod defs { mod defs {
use crate::{__composite_rejection, __define_rejection};
__define_rejection! { __define_rejection! {
#[status = BAD_REQUEST] #[status = BAD_REQUEST]
#[body = "error message 1"] #[body = "error message 1"]
+1 -1
View File
@@ -29,7 +29,7 @@ use std::fmt;
/// ) /// )
/// } /// }
/// ``` /// ```
#[derive(Debug)] #[derive(Debug, Clone, Copy)]
#[must_use] #[must_use]
pub struct AppendHeaders<I>(pub I); pub struct AppendHeaders<I>(pub I);
+2 -1
View File
@@ -49,7 +49,7 @@ use std::{
/// MyError::SomethingElseWentWrong => "something else went wrong", /// 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() /// (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
/// } /// }
/// } /// }
@@ -113,6 +113,7 @@ use std::{
/// ``` /// ```
pub trait IntoResponse { pub trait IntoResponse {
/// Create a response. /// Create a response.
#[must_use]
fn into_response(self) -> Response; fn into_response(self) -> Response;
} }
+13 -1
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 /// // the response
/// impl<'a> IntoResponse for SetHeader<'a> { /// impl<'a> IntoResponse for SetHeader<'a> {
/// fn into_response(self) -> Response { /// fn into_response(self) -> Response {
@@ -105,21 +105,25 @@ pub struct ResponseParts {
impl ResponseParts { impl ResponseParts {
/// Gets a reference to the response headers. /// Gets a reference to the response headers.
#[must_use]
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
self.res.headers() self.res.headers()
} }
/// Gets a mutable reference to the response headers. /// Gets a mutable reference to the response headers.
#[must_use]
pub fn headers_mut(&mut self) -> &mut HeaderMap { pub fn headers_mut(&mut self) -> &mut HeaderMap {
self.res.headers_mut() self.res.headers_mut()
} }
/// Gets a reference to the response extensions. /// Gets a reference to the response extensions.
#[must_use]
pub fn extensions(&self) -> &Extensions { pub fn extensions(&self) -> &Extensions {
self.res.extensions() self.res.extensions()
} }
/// Gets a mutable reference to the response extensions. /// Gets a mutable reference to the response extensions.
#[must_use]
pub fn extensions_mut(&mut self) -> &mut Extensions { pub fn extensions_mut(&mut self) -> &mut Extensions {
self.res.extensions_mut() self.res.extensions_mut()
} }
@@ -260,3 +264,11 @@ impl IntoResponseParts for Extensions {
Ok(res) Ok(res)
} }
} }
impl IntoResponseParts for () {
type Error = Infallible;
fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
Ok(res)
}
}
+1
View File
@@ -121,6 +121,7 @@ where
/// ///
/// See [`Result`] for more details. /// See [`Result`] for more details.
#[derive(Debug)] #[derive(Debug)]
#[must_use]
pub struct ErrorResponse(Response); pub struct ErrorResponse(Response);
impl<T> From<T> for ErrorResponse impl<T> From<T> for ErrorResponse
+53 -1
View File
@@ -7,7 +7,59 @@ and this project adheres to [Semantic Versioning].
# Unreleased # 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) # 0.9.1 (29. December, 2023)
+23 -15
View File
@@ -2,24 +2,26 @@
categories = ["asynchronous", "network-programming", "web-programming"] categories = ["asynchronous", "network-programming", "web-programming"]
description = "Extra utilities for axum" description = "Extra utilities for axum"
edition = "2021" edition = "2021"
rust-version = "1.66" rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum" homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"] keywords = ["http", "web", "framework"]
license = "MIT" license = "MIT"
name = "axum-extra" name = "axum-extra"
readme = "README.md" readme = "README.md"
repository = "https://github.com/tokio-rs/axum" repository = "https://github.com/tokio-rs/axum"
version = "0.9.1" version = "0.10.0-alpha.1"
[features] [features]
default = [] default = ["tracing", "multipart"]
async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"] async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"]
attachment = ["dep:tracing"]
error_response = ["dep:tracing", "tracing/std"]
cookie = ["dep:cookie"] cookie = ["dep:cookie"]
cookie-private = ["cookie", "cookie?/private"] cookie-private = ["cookie", "cookie?/private"]
cookie-signed = ["cookie", "cookie?/signed"] cookie-signed = ["cookie", "cookie?/signed"]
cookie-key-expansion = ["cookie", "cookie?/key-expansion"] 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"] form = ["dep:serde_html_form"]
json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"] json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"]
json-lines = [ json-lines = [
@@ -30,15 +32,17 @@ json-lines = [
"tokio-stream?/io-util", "tokio-stream?/io-util",
"dep:tokio", "dep:tokio",
] ]
multipart = ["dep:multer"] multipart = ["dep:multer", "dep:fastrand"]
protobuf = ["dep:prost"] protobuf = ["dep:prost"]
scheme = []
query = ["dep:serde_html_form"] query = ["dep:serde_html_form"]
tracing = ["axum-core/tracing", "axum/tracing"]
typed-header = ["dep:headers"] typed-header = ["dep:headers"]
typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"] typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"]
[dependencies] [dependencies]
axum = { path = "../axum", version = "0.7.2", default-features = false } axum = { path = "../axum", version = "0.8.0-alpha.1", default-features = false, features = ["original-uri"] }
axum-core = { path = "../axum-core", version = "0.4.2" } axum-core = { path = "../axum-core", version = "0.5.0-alpha.1" }
bytes = "1.1.0" bytes = "1.1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] } futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0" http = "1.0.0"
@@ -47,38 +51,41 @@ http-body-util = "0.1.0"
mime = "0.3" mime = "0.3"
pin-project-lite = "0.2" pin-project-lite = "0.2"
serde = "1.0" 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-layer = "0.3"
tower-service = "0.3" tower-service = "0.3"
# optional dependencies # 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 } 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 } form_urlencoded = { version = "1.1.0", optional = true }
headers = { version = "0.4.0", optional = true } headers = { version = "0.4.0", optional = true }
multer = { version = "3.0.0", optional = true } multer = { version = "3.0.0", optional = true }
percent-encoding = { version = "2.1", 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_html_form = { version = "0.2.0", optional = true }
serde_json = { version = "1.0.71", optional = true } serde_json = { version = "1.0.71", optional = true }
serde_path_to_error = { version = "0.1.8", optional = true } serde_path_to_error = { version = "0.1.8", optional = true }
tokio = { version = "1.19", optional = true } tokio = { version = "1.19", optional = true }
tokio-stream = { version = "0.1.9", optional = true } tokio-stream = { version = "0.1.9", optional = true }
tokio-util = { version = "0.7", 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] [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" 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 = { version = "1.0", features = ["derive"] }
serde_json = "1.0.71" serde_json = "1.0.71"
tokio = { version = "1.14", features = ["full"] } tokio = { version = "1.14", features = ["full"] }
tower = { version = "0.4", features = ["util"] } tower = { version = "0.5.1", features = ["util"] }
tower-http = { version = "0.5.0", features = ["map-response-body", "timeout"] } tower-http = { version = "0.6.0", features = ["map-response-body", "timeout"] }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-public-api-crates] [package.metadata.cargo-public-api-crates]
allowed = [ allowed = [
@@ -93,6 +100,7 @@ allowed = [
"headers_core", "headers_core",
"http", "http",
"http_body", "http_body",
"pin_project_lite",
"prost", "prost",
"serde", "serde",
"tokio", "tokio",
+1 -1
View File
@@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version ## Minimum supported Rust version
axum-extra's MSRV is 1.66. axum-extra's MSRV is 1.75.
## Getting Help ## Getting Help
+2 -7
View File
@@ -7,7 +7,6 @@
//! use axum::{ //! use axum::{
//! body::Bytes, //! body::Bytes,
//! Router, //! Router,
//! async_trait,
//! routing::get, //! routing::get,
//! extract::FromRequestParts, //! extract::FromRequestParts,
//! }; //! };
@@ -15,7 +14,6 @@
//! // extractors for checking permissions //! // extractors for checking permissions
//! struct AdminPermissions {} //! struct AdminPermissions {}
//! //!
//! #[async_trait]
//! impl<S> FromRequestParts<S> for AdminPermissions //! impl<S> FromRequestParts<S> for AdminPermissions
//! where //! where
//! S: Send + Sync, //! S: Send + Sync,
@@ -29,7 +27,6 @@
//! //!
//! struct User {} //! struct User {}
//! //!
//! #[async_trait]
//! impl<S> FromRequestParts<S> for User //! impl<S> FromRequestParts<S> for User
//! where //! where
//! S: Send + Sync, //! S: Send + Sync,
@@ -96,7 +93,6 @@
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use axum::{ use axum::{
async_trait,
extract::FromRequestParts, extract::FromRequestParts,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
@@ -236,7 +232,6 @@ macro_rules! impl_traits_for_either {
[$($ident:ident),* $(,)?], [$($ident:ident),* $(,)?],
$last:ident $(,)? $last:ident $(,)?
) => { ) => {
#[async_trait]
impl<S, $($ident),*, $last> FromRequestParts<S> for $either<$($ident),*, $last> impl<S, $($ident),*, $last> FromRequestParts<S> for $either<$($ident),*, $last>
where where
$($ident: FromRequestParts<S>),*, $($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> { 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)); 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)
} }
} }
+2 -11
View File
@@ -1,7 +1,4 @@
use axum::{ use axum::extract::{Extension, FromRequestParts};
async_trait,
extract::{Extension, FromRequestParts},
};
use http::request::Parts; use http::request::Parts;
/// Cache results of other extractors. /// Cache results of other extractors.
@@ -19,7 +16,6 @@ use http::request::Parts;
/// ```rust /// ```rust
/// use axum_extra::extract::Cached; /// use axum_extra::extract::Cached;
/// use axum::{ /// use axum::{
/// async_trait,
/// extract::FromRequestParts, /// extract::FromRequestParts,
/// response::{IntoResponse, Response}, /// response::{IntoResponse, Response},
/// http::{StatusCode, request::Parts}, /// http::{StatusCode, request::Parts},
@@ -28,7 +24,6 @@ use http::request::Parts;
/// #[derive(Clone)] /// #[derive(Clone)]
/// struct Session { /* ... */ } /// struct Session { /* ... */ }
/// ///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for Session /// impl<S> FromRequestParts<S> for Session
/// where /// where
/// S: Send + Sync, /// S: Send + Sync,
@@ -43,7 +38,6 @@ use http::request::Parts;
/// ///
/// struct CurrentUser { /* ... */ } /// struct CurrentUser { /* ... */ }
/// ///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for CurrentUser /// impl<S> FromRequestParts<S> for CurrentUser
/// where /// where
/// S: Send + Sync, /// S: Send + Sync,
@@ -86,7 +80,6 @@ pub struct Cached<T>(pub T);
#[derive(Clone)] #[derive(Clone)]
struct CachedEntry<T>(T); struct CachedEntry<T>(T);
#[async_trait]
impl<S, T> FromRequestParts<S> for Cached<T> impl<S, T> FromRequestParts<S> for Cached<T>
where where
S: Send + Sync, S: Send + Sync,
@@ -111,8 +104,7 @@ axum_core::__impl_deref!(Cached);
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use axum::{extract::FromRequestParts, http::Request, routing::get, Router}; use axum::{http::Request, routing::get, Router};
use http::request::Parts;
use std::{ use std::{
convert::Infallible, convert::Infallible,
sync::atomic::{AtomicU32, Ordering}, sync::atomic::{AtomicU32, Ordering},
@@ -126,7 +118,6 @@ mod tests {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
struct Extractor(Instant); struct Extractor(Instant);
#[async_trait]
impl<S> FromRequestParts<S> for Extractor impl<S> FromRequestParts<S> for Extractor
where where
S: Send + Sync, S: Send + Sync,
-2
View File
@@ -3,7 +3,6 @@
//! See [`CookieJar`], [`SignedCookieJar`], and [`PrivateCookieJar`] for more details. //! See [`CookieJar`], [`SignedCookieJar`], and [`PrivateCookieJar`] for more details.
use axum::{ use axum::{
async_trait,
extract::FromRequestParts, extract::FromRequestParts,
response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
}; };
@@ -90,7 +89,6 @@ pub struct CookieJar {
jar: cookie::CookieJar, jar: cookie::CookieJar,
} }
#[async_trait]
impl<S> FromRequestParts<S> for CookieJar impl<S> FromRequestParts<S> for CookieJar
where where
S: Send + Sync, S: Send + Sync,
+3 -5
View File
@@ -1,6 +1,5 @@
use super::{cookies_from_request, set_cookies, Cookie, Key}; use super::{cookies_from_request, set_cookies, Cookie, Key};
use axum::{ use axum::{
async_trait,
extract::{FromRef, FromRequestParts}, extract::{FromRef, FromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
}; };
@@ -49,11 +48,11 @@ use std::{convert::Infallible, fmt, marker::PhantomData};
/// // our application state /// // our application state
/// #[derive(Clone)] /// #[derive(Clone)]
/// struct AppState { /// struct AppState {
/// // that holds the key used to sign cookies /// // that holds the key used to encrypt cookies
/// key: Key, /// 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 { /// impl FromRef<AppState> for Key {
/// fn from_ref(state: &AppState) -> Self { /// fn from_ref(state: &AppState) -> Self {
/// state.key.clone() /// state.key.clone()
@@ -122,7 +121,6 @@ impl<K> fmt::Debug for PrivateCookieJar<K> {
} }
} }
#[async_trait]
impl<S, K> FromRequestParts<S> for PrivateCookieJar<K> impl<S, K> FromRequestParts<S> for PrivateCookieJar<K>
where where
S: Send + Sync, S: Send + Sync,
@@ -291,7 +289,7 @@ struct PrivateCookieJarIter<'a, K> {
iter: cookie::Iter<'a>, iter: cookie::Iter<'a>,
} }
impl<'a, K> Iterator for PrivateCookieJarIter<'a, K> { impl<K> Iterator for PrivateCookieJarIter<'_, K> {
type Item = Cookie<'static>; type Item = Cookie<'static>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
+1 -3
View File
@@ -1,6 +1,5 @@
use super::{cookies_from_request, set_cookies}; use super::{cookies_from_request, set_cookies};
use axum::{ use axum::{
async_trait,
extract::{FromRef, FromRequestParts}, extract::{FromRef, FromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, 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> impl<S, K> FromRequestParts<S> for SignedCookieJar<K>
where where
S: Send + Sync, S: Send + Sync,
@@ -309,7 +307,7 @@ struct SignedCookieJarIter<'a, K> {
iter: cookie::Iter<'a>, iter: cookie::Iter<'a>,
} }
impl<'a, K> Iterator for SignedCookieJarIter<'a, K> { impl<K> Iterator for SignedCookieJarIter<'_, K> {
type Item = Cookie<'static>; type Item = Cookie<'static>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
+11 -9
View File
@@ -1,5 +1,4 @@
use axum::{ use axum::{
async_trait,
extract::{rejection::RawFormRejection, FromRequest, RawForm, Request}, extract::{rejection::RawFormRejection, FromRequest, RawForm, Request},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Error, RequestExt, Error, RequestExt,
@@ -44,7 +43,6 @@ pub struct Form<T>(pub T);
axum_core::__impl_deref!(Form); axum_core::__impl_deref!(Form);
#[async_trait]
impl<T, S> FromRequest<S> for Form<T> impl<T, S> FromRequest<S> for Form<T>
where where
T: DeserializeOwned, T: DeserializeOwned,
@@ -81,11 +79,16 @@ impl IntoResponse for FormRejection {
fn into_response(self) -> Response { fn into_response(self) -> Response {
match self { match self {
Self::RawFormRejection(inner) => inner.into_response(), Self::RawFormRejection(inner) => inner.into_response(),
Self::FailedToDeserializeForm(inner) => ( Self::FailedToDeserializeForm(inner) => {
StatusCode::BAD_REQUEST, let body = format!("Failed to deserialize form: {inner}");
format!("Failed to deserialize form: {inner}"), let status = StatusCode::BAD_REQUEST;
) axum_core::__log_rejection!(
.into_response(), rejection_type = Self,
body_text = body,
status = status,
);
(status, body).into_response()
}
} }
} }
} }
@@ -113,7 +116,7 @@ mod tests {
use super::*; use super::*;
use crate::test_helpers::*; use crate::test_helpers::*;
use axum::{routing::post, Router}; use axum::{routing::post, Router};
use http::{header::CONTENT_TYPE, StatusCode}; use http::header::CONTENT_TYPE;
use serde::Deserialize; use serde::Deserialize;
#[tokio::test] #[tokio::test]
@@ -135,7 +138,6 @@ mod tests {
.post("/") .post("/")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("value=one&value=two") .body("value=one&value=two")
.send()
.await; .await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
@@ -1,29 +1,29 @@
use super::{ use super::rejection::{FailedToResolveHost, HostRejection};
rejection::{FailedToResolveHost, HostRejection}, use axum::extract::FromRequestParts;
FromRequestParts,
};
use async_trait::async_trait;
use http::{ use http::{
header::{HeaderMap, FORWARDED}, header::{HeaderMap, FORWARDED},
request::Parts, request::Parts,
uri::Authority,
}; };
const X_FORWARDED_HOST_HEADER_KEY: &str = "X-Forwarded-Host"; 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 /// - `Forwarded` header
/// - `X-Forwarded-Host` header /// - `X-Forwarded-Host` header
/// - `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 /// 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. /// sure to validate them to avoid security issues.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Host(pub String); pub struct Host(pub String);
#[async_trait]
impl<S> FromRequestParts<S> for Host impl<S> FromRequestParts<S> for Host
where where
S: Send + Sync, S: Send + Sync,
@@ -51,8 +51,8 @@ where
return Ok(Host(host.to_owned())); return Ok(Host(host.to_owned()));
} }
if let Some(host) = parts.uri.host() { if let Some(authority) = parts.uri.authority() {
return Ok(Host(host.to_owned())); return Ok(Host(parse_authority(authority).to_owned()));
} }
Err(HostRejection::FailedToResolveHost(FailedToResolveHost)) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{routing::get, test_helpers::TestClient, Router}; use crate::test_helpers::TestClient;
use http::header::HeaderName; use axum::{routing::get, Router};
use http::{header::HeaderName, Request};
fn test_client() -> TestClient { fn test_client() -> TestClient {
async fn host_as_body(Host(host): Host) -> String { async fn host_as_body(Host(host): Host) -> String {
@@ -96,7 +104,6 @@ mod tests {
let host = test_client() let host = test_client()
.get("/") .get("/")
.header(http::header::HOST, original_host) .header(http::header::HOST, original_host)
.send()
.await .await
.text() .text()
.await; .await;
@@ -109,7 +116,6 @@ mod tests {
let host = test_client() let host = test_client()
.get("/") .get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, original_host) .header(X_FORWARDED_HOST_HEADER_KEY, original_host)
.send()
.await .await
.text() .text()
.await; .await;
@@ -124,7 +130,6 @@ mod tests {
.get("/") .get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header) .header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header)
.header(http::header::HOST, host_header) .header(http::header::HOST, host_header)
.send()
.await .await
.text() .text()
.await; .await;
@@ -133,8 +138,26 @@ mod tests {
#[crate::test] #[crate::test]
async fn uri_host() { async fn uri_host() {
let host = test_client().get("/").send().await.text().await; let client = test_client();
assert!(host.contains("127.0.0.1")); 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] #[test]
+8 -26
View File
@@ -1,4 +1,3 @@
use axum::async_trait;
use axum::extract::{FromRequest, Request}; use axum::extract::{FromRequest, Request};
use axum_core::__composite_rejection as composite_rejection; use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection; use axum_core::__define_rejection as define_rejection;
@@ -10,7 +9,7 @@ use std::marker::PhantomData;
/// JSON Extractor for zero-copy deserialization. /// 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. /// Parsing JSON is delayed until [`deserialize`](JsonDeserializer::deserialize) is called.
/// If the type implements [`serde::de::DeserializeOwned`], the [`Json`](axum::Json) extractor should /// If the type implements [`serde::de::DeserializeOwned`], the [`Json`](axum::Json) extractor should
/// be preferred. /// be preferred.
@@ -23,8 +22,7 @@ use std::marker::PhantomData;
/// Additionally, a `JsonRejection` error will be returned, when calling `deserialize` if: /// Additionally, a `JsonRejection` error will be returned, when calling `deserialize` if:
/// ///
/// - The body doesn't contain syntactically valid JSON. /// - The body doesn't contain syntactically valid JSON.
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target /// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target type.
/// type.
/// - Attempting to deserialize escaped JSON into a type that must be borrowed (e.g. `&'a str`). /// - 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 /// ⚠️ `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>, _marker: PhantomData<T>,
} }
#[async_trait]
impl<T, S> FromRequest<S> for JsonDeserializer<T> impl<T, S> FromRequest<S> for JsonDeserializer<T>
where where
T: Deserialize<'static>, T: Deserialize<'static>,
@@ -205,7 +202,7 @@ fn json_content_type(headers: &HeaderMap) -> bool {
}; };
let is_json_content_type = mime.type_() == "application" 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 is_json_content_type
} }
@@ -245,7 +242,7 @@ mod tests {
let app = Router::new().route("/", post(handler)); let app = Router::new().route("/", post(handler));
let client = TestClient::new(app); 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; let body = res.text().await;
assert_eq!(body, "bar"); assert_eq!(body, "bar");
@@ -277,11 +274,7 @@ mod tests {
let client = TestClient::new(app); let client = TestClient::new(app);
// The escaped characters prevent serde_json from borrowing. // The escaped characters prevent serde_json from borrowing.
let res = client let res = client.post("/").json(&json!({ "foo": "\"bar\"" })).await;
.post("/")
.json(&json!({ "foo": "\"bar\"" }))
.send()
.await;
let body = res.text().await; let body = res.text().await;
@@ -308,19 +301,11 @@ mod tests {
let client = TestClient::new(app); let client = TestClient::new(app);
let res = client let res = client.post("/").json(&json!({ "foo": "good" })).await;
.post("/")
.json(&json!({ "foo": "good" }))
.send()
.await;
let body = res.text().await; let body = res.text().await;
assert_eq!(body, "good"); assert_eq!(body, "good");
let res = client let res = client.post("/").json(&json!({ "foo": "\"bad\"" })).await;
.post("/")
.json(&json!({ "foo": "\"bad\"" }))
.send()
.await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let body_text = res.text().await; let body_text = res.text().await;
assert_eq!( assert_eq!(
@@ -344,7 +329,7 @@ mod tests {
let app = Router::new().route("/", post(handler)); let app = Router::new().route("/", post(handler));
let client = TestClient::new(app); 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(); let status = res.status();
@@ -366,7 +351,6 @@ mod tests {
.post("/") .post("/")
.header("content-type", content_type) .header("content-type", content_type)
.body("{}") .body("{}")
.send()
.await; .await;
res.status() == StatusCode::OK res.status() == StatusCode::OK
@@ -395,7 +379,6 @@ mod tests {
.post("/") .post("/")
.body("{") .body("{")
.header("content-type", "application/json") .header("content-type", "application/json")
.send()
.await; .await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@@ -433,7 +416,6 @@ mod tests {
.post("/") .post("/")
.body("{\"a\": 1, \"b\": [{\"x\": 2}]}") .body("{\"a\": 1, \"b\": [{\"x\": 2}]}")
.header("content-type", "application/json") .header("content-type", "application/json")
.send()
.await; .await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
+12 -1
View File
@@ -1,7 +1,9 @@
//! Additional extractors. //! Additional extractors.
mod cached; mod cached;
mod host;
mod optional_path; mod optional_path;
pub mod rejection;
mod with_rejection; mod with_rejection;
#[cfg(feature = "form")] #[cfg(feature = "form")]
@@ -19,7 +21,12 @@ mod query;
#[cfg(feature = "multipart")] #[cfg(feature = "multipart")]
pub mod 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")] #[cfg(feature = "cookie")]
pub use self::cookie::CookieJar; pub use self::cookie::CookieJar;
@@ -39,6 +46,10 @@ pub use self::query::{OptionalQuery, OptionalQueryRejection, Query, QueryRejecti
#[cfg(feature = "multipart")] #[cfg(feature = "multipart")]
pub use self::multipart::Multipart; pub use self::multipart::Multipart;
#[cfg(feature = "scheme")]
#[doc(no_inline)]
pub use self::scheme::{Scheme, SchemeMissing};
#[cfg(feature = "json-deserializer")] #[cfg(feature = "json-deserializer")]
pub use self::json_deserializer::{ pub use self::json_deserializer::{
JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError, JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError,
+11 -7
View File
@@ -3,7 +3,6 @@
//! See [`Multipart`] for more details. //! See [`Multipart`] for more details.
use axum::{ use axum::{
async_trait,
body::{Body, Bytes}, body::{Body, Bytes},
extract::FromRequest, extract::FromRequest,
response::{IntoResponse, Response}, 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 /// to keep `Field`s around from previous loop iterations. That will minimize the risk of runtime
/// errors. /// 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 /// `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`. /// that leads to significant usability issues such as `Field` not being `'static`.
@@ -90,7 +89,6 @@ pub struct Multipart {
inner: multer::Multipart<'static>, inner: multer::Multipart<'static>,
} }
#[async_trait]
impl<S> FromRequest<S> for Multipart impl<S> FromRequest<S> for Multipart
where where
S: Send + Sync, S: Send + Sync,
@@ -379,7 +377,13 @@ pub struct InvalidBoundary;
impl IntoResponse for InvalidBoundary { impl IntoResponse for InvalidBoundary {
fn into_response(self) -> Response { 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 { mod tests {
use super::*; use super::*;
use crate::test_helpers::*; use crate::test_helpers::*;
use axum::{extract::DefaultBodyLimit, response::IntoResponse, routing::post, Router}; use axum::{extract::DefaultBodyLimit, routing::post, Router};
#[tokio::test] #[tokio::test]
async fn content_type_with_encoding() { async fn content_type_with_encoding() {
@@ -437,7 +441,7 @@ mod tests {
.unwrap(), .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 // No need for this to be a #[test], we just want to make sure it compiles
@@ -466,7 +470,7 @@ mod tests {
let form = let form =
reqwest::multipart::Form::new().part("file", reqwest::multipart::Part::bytes(BYTES)); 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); assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
} }
} }
+7 -9
View File
@@ -1,5 +1,4 @@
use axum::{ use axum::{
async_trait,
extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Path}, extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Path},
RequestPartsExt, RequestPartsExt,
}; };
@@ -29,13 +28,12 @@ use serde::de::DeserializeOwned;
/// ///
/// let app = Router::new() /// let app = Router::new()
/// .route("/blog", get(render_blog)) /// .route("/blog", get(render_blog))
/// .route("/blog/:page", get(render_blog)); /// .route("/blog/{page}", get(render_blog));
/// # let app: Router = app; /// # let app: Router = app;
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
pub struct OptionalPath<T>(pub Option<T>); pub struct OptionalPath<T>(pub Option<T>);
#[async_trait]
impl<T, S> FromRequestParts<S> for OptionalPath<T> impl<T, S> FromRequestParts<S> for OptionalPath<T>
where where
T: DeserializeOwned + Send + 'static, T: DeserializeOwned + Send + 'static,
@@ -77,26 +75,26 @@ mod tests {
let app = Router::new() let app = Router::new()
.route("/", get(handle)) .route("/", get(handle))
.route("/:num", get(handle)); .route("/{num}", get(handle));
let client = TestClient::new(app); let client = TestClient::new(app);
let res = client.get("/").send().await; let res = client.get("/").await;
assert_eq!(res.text().await, "Success: 0"); 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"); assert_eq!(res.text().await, "Success: 1");
let res = client.get("/0").send().await; let res = client.get("/0").await;
assert_eq!( assert_eq!(
res.text().await, res.text().await,
"Invalid URL: invalid value: integer `0`, expected a nonzero u32" "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!( assert_eq!(
res.text().await, res.text().await,
"Invalid URL: Cannot parse `\"NaN\"` to a `u32`" "Invalid URL: Cannot parse `NaN` to a `u32`"
); );
} }
} }
+39 -13
View File
@@ -1,5 +1,4 @@
use axum::{ use axum::{
async_trait,
extract::FromRequestParts, extract::FromRequestParts,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Error, Error,
@@ -51,11 +50,37 @@ use std::fmt;
/// example. /// example.
/// ///
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs /// [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")))] #[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct Query<T>(pub T); pub struct Query<T>(pub T);
#[async_trait]
impl<T, S> FromRequestParts<S> for Query<T> impl<T, S> FromRequestParts<S> for Query<T>
where where
T: DeserializeOwned, T: DeserializeOwned,
@@ -87,11 +112,16 @@ pub enum QueryRejection {
impl IntoResponse for QueryRejection { impl IntoResponse for QueryRejection {
fn into_response(self) -> Response { fn into_response(self) -> Response {
match self { match self {
Self::FailedToDeserializeQueryString(inner) => ( Self::FailedToDeserializeQueryString(inner) => {
StatusCode::BAD_REQUEST, let body = format!("Failed to deserialize query string: {inner}");
format!("Failed to deserialize query string: {inner}"), let status = StatusCode::BAD_REQUEST;
) axum_core::__log_rejection!(
.into_response(), 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)] #[derive(Debug, Clone, Copy, Default)]
pub struct OptionalQuery<T>(pub Option<T>); pub struct OptionalQuery<T>(pub Option<T>);
#[async_trait]
impl<T, S> FromRequestParts<S> for OptionalQuery<T> impl<T, S> FromRequestParts<S> for OptionalQuery<T>
where where
T: DeserializeOwned, T: DeserializeOwned,
@@ -235,7 +264,7 @@ mod tests {
use super::*; use super::*;
use crate::test_helpers::*; use crate::test_helpers::*;
use axum::{routing::post, Router}; use axum::{routing::post, Router};
use http::{header::CONTENT_TYPE, StatusCode}; use http::header::CONTENT_TYPE;
use serde::Deserialize; use serde::Deserialize;
#[tokio::test] #[tokio::test]
@@ -257,7 +286,6 @@ mod tests {
.post("/?value=one&value=two") .post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("") .body("")
.send()
.await; .await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
@@ -286,7 +314,6 @@ mod tests {
.post("/?value=one&value=two") .post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("") .body("")
.send()
.await; .await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
@@ -312,7 +339,7 @@ mod tests {
let client = TestClient::new(app); 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.status(), StatusCode::OK);
assert_eq!(res.text().await, "None"); assert_eq!(res.text().await, "None");
@@ -341,7 +368,6 @@ mod tests {
.post("/?other=something") .post("/?other=something")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("") .body("")
.send()
.await; .await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!(res.status(), StatusCode::BAD_REQUEST);
+23
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,
}
}
+152
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
}
}
+22 -9
View File
@@ -1,11 +1,13 @@
use axum::async_trait;
use axum::extract::{FromRequest, FromRequestParts, Request}; use axum::extract::{FromRequest, FromRequestParts, Request};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use http::request::Parts; use http::request::Parts;
use std::fmt::Debug; use std::fmt::{Debug, Display};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
#[cfg(feature = "typed-routing")]
use crate::routing::TypedPath;
/// Extractor for customizing extractor rejections /// Extractor for customizing extractor rejections
/// ///
/// `WithRejection` wraps another extractor and gives you the result. If the /// `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> impl<E, R, S> FromRequest<S> for WithRejection<E, R>
where where
S: Send + Sync, S: Send + Sync,
@@ -122,7 +123,6 @@ where
} }
} }
#[async_trait]
impl<E, R, S> FromRequestParts<S> for WithRejection<E, R> impl<E, R, S> FromRequestParts<S> for WithRejection<E, R>
where where
S: Send + Sync, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use axum::body::Body; use axum::body::Body;
use axum::extract::FromRequestParts;
use axum::http::Request; use axum::http::Request;
use axum::response::Response; use axum::response::Response;
use http::request::Parts;
use super::*;
#[tokio::test] #[tokio::test]
async fn extractor_rejection_is_transformed() { async fn extractor_rejection_is_transformed() {
struct TestExtractor; struct TestExtractor;
struct TestRejection; struct TestRejection;
#[async_trait]
impl<S> FromRequestParts<S> for TestExtractor impl<S> FromRequestParts<S> for TestExtractor
where where
S: Send + Sync, S: Send + Sync,
+2 -5
View File
@@ -47,7 +47,6 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// use axum_extra::handler::HandlerCallWithExtractors; /// use axum_extra::handler::HandlerCallWithExtractors;
/// use axum::{ /// use axum::{
/// Router, /// Router,
/// async_trait,
/// routing::get, /// routing::get,
/// extract::FromRequestParts, /// extract::FromRequestParts,
/// }; /// };
@@ -68,7 +67,6 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// // extractors for checking permissions /// // extractors for checking permissions
/// struct AdminPermissions {} /// struct AdminPermissions {}
/// ///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for AdminPermissions /// impl<S> FromRequestParts<S> for AdminPermissions
/// where /// where
/// S: Send + Sync, /// S: Send + Sync,
@@ -82,7 +80,6 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// ///
/// struct User {} /// struct User {}
/// ///
/// #[async_trait]
/// impl<S> FromRequestParts<S> for User /// impl<S> FromRequestParts<S> for User
/// where /// where
/// S: Send + Sync, /// S: Send + Sync,
@@ -95,7 +92,7 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// } /// }
/// ///
/// let app = Router::new().route( /// let app = Router::new().route(
/// "/users/:id", /// "/users/{id}",
/// get( /// get(
/// // first try `admin`, if that rejects run `user`, finally falling back /// // first try `admin`, if that rejects run `user`, finally falling back
/// // to `guest` /// // to `guest`
@@ -168,7 +165,7 @@ pub struct IntoHandler<H, T, S> {
impl<H, T, S> Handler<T, S> for IntoHandler<H, T, S> impl<H, T, S> Handler<T, S> for IntoHandler<H, T, S>
where where
H: HandlerCallWithExtractors<T, S> + Clone + Send + 'static, H: HandlerCallWithExtractors<T, S> + Clone + Send + Sync + 'static,
T: FromRequest<S> + Send + 'static, T: FromRequest<S> + Send + 'static,
T::Rejection: Send, T::Rejection: Send,
S: Send + Sync + 'static, S: Send + Sync + 'static,
+6 -6
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> impl<S, L, R, Lt, Rt, M> Handler<(M, Lt, Rt), S> for Or<L, R, Lt, Rt, S>
where where
L: HandlerCallWithExtractors<Lt, S> + Clone + Send + 'static, L: HandlerCallWithExtractors<Lt, S> + Clone + Send + Sync + 'static,
R: HandlerCallWithExtractors<Rt, S> + Clone + Send + 'static, R: HandlerCallWithExtractors<Rt, S> + Clone + Send + Sync + 'static,
Lt: FromRequestParts<S> + Send + 'static, Lt: FromRequestParts<S> + Send + 'static,
Rt: FromRequest<S, M> + Send + 'static, Rt: FromRequest<S, M> + Send + 'static,
Lt::Rejection: Send, Lt::Rejection: Send,
@@ -134,17 +134,17 @@ mod tests {
"fallback" "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 client = TestClient::new(app);
let res = client.get("/123").send().await; let res = client.get("/123").await;
assert_eq!(res.text().await, "123"); 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"); 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"); assert_eq!(res.text().await, "fallback");
} }
} }
+3 -6
View File
@@ -1,7 +1,6 @@
//! Newline delimited JSON extractor and response. //! Newline delimited JSON extractor and response.
use axum::{ use axum::{
async_trait,
body::Body, body::Body,
extract::{FromRequest, Request}, extract::{FromRequest, Request},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
@@ -55,7 +54,7 @@ pin_project! {
/// JsonLines::new(stream_of_values()).into_response() /// 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 // as an extractor
#[must_use] #[must_use]
pub struct JsonLines<S, T = AsExtractor> { 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> impl<S, T> FromRequest<S> for JsonLines<T, AsExtractor>
where where
T: DeserializeOwned, T: DeserializeOwned,
@@ -184,7 +182,7 @@ mod tests {
use futures_util::StreamExt; use futures_util::StreamExt;
use http::StatusCode; use http::StatusCode;
use serde::Deserialize; use serde::Deserialize;
use std::{convert::Infallible, error::Error}; use std::error::Error;
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct User { struct User {
@@ -224,7 +222,6 @@ mod tests {
] ]
.join("\n"), .join("\n"),
) )
.send()
.await; .await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
} }
@@ -245,7 +242,7 @@ mod tests {
let client = TestClient::new(app); let client = TestClient::new(app);
let res = client.get("/").send().await; let res = client.get("/").await;
let values = res let values = res
.text() .text()
+18 -19
View File
@@ -9,20 +9,22 @@
//! //!
//! Name | Description | Default? //! Name | Description | Default?
//! ---|---|--- //! ---|---|---
//! `async-read-body` | Enables the `AsyncReadBody` body | No //! `async-read-body` | Enables the [`AsyncReadBody`](crate::body::AsyncReadBody) body | No
//! `cookie` | Enables the `CookieJar` extractor | No //! `attachment` | Enables the [`Attachment`](crate::response::Attachment) response | No
//! `cookie-private` | Enables the `PrivateCookieJar` extractor | No //! `cookie` | Enables the [`CookieJar`](crate::extract::CookieJar) extractor | No
//! `cookie-signed` | Enables the `SignedCookieJar` extractor | No //! `cookie-private` | Enables the [`PrivateCookieJar`](crate::extract::PrivateCookieJar) extractor | No
//! `cookie-key-expansion` | Enables the `Key::derive_from` method | No //! `cookie-signed` | Enables the [`SignedCookieJar`](crate::extract::SignedCookieJar) extractor | No
//! `erased-json` | Enables the `ErasedJson` response | No //! `cookie-key-expansion` | Enables the [`Key::derive_from`](crate::extract::cookie::Key::derive_from) method | No
//! `form` | Enables the `Form` extractor | No //! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response | No
//! `json-deserializer` | Enables the `JsonDeserializer` extractor | No //! `form` | Enables the [`Form`](crate::extract::Form) extractor | No
//! `json-lines` | Enables the `JsonLines` extractor and response | No //! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor | No
//! `multipart` | Enables the `Multipart` extractor | No //! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response | No
//! `protobuf` | Enables the `Protobuf` extractor and response | No //! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor | No
//! `query` | Enables the `Query` extractor | No //! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response | No
//! `typed-routing` | Enables the `TypedPath` routing utilities | No //! `query` | Enables the [`Query`](crate::extract::Query) extractor | No
//! `typed-header` | Enables the `TypedHeader` extractor and response | 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 //! [`axum`]: https://crates.io/crates/axum
@@ -39,7 +41,6 @@
clippy::needless_borrow, clippy::needless_borrow,
clippy::match_wildcard_for_single_variants, clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex, clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock, clippy::await_holding_lock,
clippy::match_on_vec_items, clippy::match_on_vec_items,
clippy::imprecise_flops, clippy::imprecise_flops,
@@ -96,11 +97,10 @@ pub use typed_header::TypedHeader;
#[cfg(feature = "protobuf")] #[cfg(feature = "protobuf")]
pub mod protobuf; pub mod protobuf;
/// _not_ public API
#[cfg(feature = "typed-routing")] #[cfg(feature = "typed-routing")]
#[doc(hidden)] #[doc(hidden)]
pub mod __private { pub mod __private {
//! _not_ public API
use percent_encoding::{AsciiSet, CONTROLS}; use percent_encoding::{AsciiSet, CONTROLS};
pub use percent_encoding::utf8_percent_encode; pub use percent_encoding::utf8_percent_encode;
@@ -115,9 +115,8 @@ pub mod __private {
use axum_macros::__private_axum_test as test; use axum_macros::__private_axum_test as test;
#[cfg(test)] #[cfg(test)]
#[allow(unused_imports)]
pub(crate) mod test_helpers { pub(crate) mod test_helpers {
#![allow(unused_imports)]
use axum::{extract::Request, response::Response, serve}; use axum::{extract::Request, response::Response, serve};
mod test_client { mod test_client {
+4 -7
View File
@@ -1,7 +1,6 @@
//! Protocol Buffer extractor and response. //! Protocol Buffer extractor and response.
use axum::{ use axum::{
async_trait,
extract::{rejection::BytesRejection, FromRequest, Request}, extract::{rejection::BytesRejection, FromRequest, Request},
response::{IntoResponse, IntoResponseFailed, Response}, response::{IntoResponse, IntoResponseFailed, Response},
}; };
@@ -82,7 +81,7 @@ use prost::Message;
/// # unimplemented!() /// # unimplemented!()
/// } /// }
/// ///
/// let app = Router::new().route("/users/:id", get(get_user)); /// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app; /// # let _: Router = app;
/// ``` /// ```
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
@@ -90,7 +89,6 @@ use prost::Message;
#[must_use] #[must_use]
pub struct Protobuf<T>(pub T); pub struct Protobuf<T>(pub T);
#[async_trait]
impl<T, S> FromRequest<S> for Protobuf<T> impl<T, S> FromRequest<S> for Protobuf<T>
where where
T: Message + Default, T: Message + Default,
@@ -206,7 +204,6 @@ mod tests {
use super::*; use super::*;
use crate::test_helpers::*; use crate::test_helpers::*;
use axum::{routing::post, Router}; use axum::{routing::post, Router};
use http::StatusCode;
#[tokio::test] #[tokio::test]
async fn decode_body() { async fn decode_body() {
@@ -226,7 +223,7 @@ mod tests {
}; };
let client = TestClient::new(app); 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; let body = res.text().await;
@@ -254,7 +251,7 @@ mod tests {
}; };
let client = TestClient::new(app); 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); assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
} }
@@ -289,7 +286,7 @@ mod tests {
}; };
let client = TestClient::new(app); 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!( assert_eq!(
res.headers()["content-type"], res.headers()["content-type"],
+103
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()
}
}
+71
View File
@@ -12,6 +12,15 @@ use serde::Serialize;
/// This allows returning a borrowing type from a handler, or returning different response /// This allows returning a borrowing type from a handler, or returning different response
/// types as JSON from different branches inside a handler. /// 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 /// # Example
/// ///
/// ```rust /// ```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;
}
+51
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);
}
}
+20 -8
View File
@@ -3,13 +3,33 @@
#[cfg(feature = "erased-json")] #[cfg(feature = "erased-json")]
mod 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")] #[cfg(feature = "erased-json")]
pub use erased_json::ErasedJson; 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")] #[cfg(feature = "json-lines")]
#[doc(no_inline)] #[doc(no_inline)]
pub use crate::json_lines::JsonLines; pub use crate::json_lines::JsonLines;
#[cfg(feature = "attachment")]
pub use attachment::Attachment;
macro_rules! mime_response { macro_rules! mime_response {
( (
$(#[$m:meta])* $(#[$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! { mime_response! {
/// A JavaScript response. /// A JavaScript response.
/// ///
+295
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."
);
}
}
}
+55 -16
View File
@@ -1,7 +1,7 @@
//! Additional types for defining routes. //! Additional types for defining routes.
use axum::{ use axum::{
extract::Request, extract::{OriginalUri, Request},
response::{IntoResponse, Redirect, Response}, response::{IntoResponse, Redirect, Response},
routing::{any, MethodRouter}, routing::{any, MethodRouter},
Router, Router,
@@ -131,6 +131,19 @@ pub trait RouterExt<S>: sealed::Sealed {
T: SecondElementIs<P> + 'static, T: SecondElementIs<P> + 'static,
P: TypedPath; 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. /// 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 /// 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`]. /// This works like [`RouterExt::route_with_tsr`] but accepts any [`Service`].
fn route_service_with_tsr<T>(self, path: &str, service: T) -> Self fn route_service_with_tsr<T>(self, path: &str, service: T) -> Self
where where
T: Service<Request, Error = Infallible> + Clone + Send + 'static, T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse, T::Response: IntoResponse,
T::Future: Send + 'static, T::Future: Send + 'static,
Self: Sized; Self: Sized;
@@ -255,6 +268,16 @@ where
self.route(P::PATH, axum::routing::trace(handler)) 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] #[track_caller]
fn route_with_tsr(mut self, path: &str, method_router: MethodRouter<S>) -> Self fn route_with_tsr(mut self, path: &str, method_router: MethodRouter<S>) -> Self
where where
@@ -268,7 +291,7 @@ where
#[track_caller] #[track_caller]
fn route_service_with_tsr<T>(mut self, path: &str, service: T) -> Self fn route_service_with_tsr<T>(mut self, path: &str, service: T) -> Self
where where
T: Service<Request, Error = Infallible> + Clone + Send + 'static, T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse, T::Response: IntoResponse,
T::Future: Send + 'static, T::Future: Send + 'static,
Self: Sized, Self: Sized,
@@ -290,7 +313,7 @@ fn add_tsr_redirect_route<S>(router: Router<S>, path: &str) -> Router<S>
where where
S: Clone + Send + Sync + 'static, 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| { let new_uri = map_path(uri, |path| {
path.strip_suffix('/') path.strip_suffix('/')
.map(Cow::Borrowed) .map(Cow::Borrowed)
@@ -342,7 +365,7 @@ mod sealed {
mod tests { mod tests {
use super::*; use super::*;
use crate::test_helpers::*; use crate::test_helpers::*;
use axum::{extract::Path, http::StatusCode, routing::get}; use axum::{extract::Path, routing::get};
#[tokio::test] #[tokio::test]
async fn test_tsr() { async fn test_tsr() {
@@ -352,17 +375,17 @@ mod tests {
let client = TestClient::new(app); let client = TestClient::new(app);
let res = client.get("/foo").send().await; let res = client.get("/foo").await;
assert_eq!(res.status(), StatusCode::OK); 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.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/foo"); 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); 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.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/bar/"); assert_eq!(res.headers()["location"], "/bar/");
} }
@@ -371,29 +394,29 @@ mod tests {
async fn tsr_with_params() { async fn tsr_with_params() {
let app = Router::new() let app = Router::new()
.route_with_tsr( .route_with_tsr(
"/a/:a", "/a/{a}",
get(|Path(param): Path<String>| async move { param }), get(|Path(param): Path<String>| async move { param }),
) )
.route_with_tsr( .route_with_tsr(
"/b/:b/", "/b/{b}/",
get(|Path(param): Path<String>| async move { param }), get(|Path(param): Path<String>| async move { param }),
); );
let client = TestClient::new(app); 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.status(), StatusCode::OK);
assert_eq!(res.text().await, "foo"); 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.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/a/foo"); 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.status(), StatusCode::OK);
assert_eq!(res.text().await, "foo"); 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.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/b/foo/"); assert_eq!(res.headers()["location"], "/b/foo/");
} }
@@ -404,11 +427,27 @@ mod tests {
let client = TestClient::new(app); 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.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/foo?a=a"); 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] #[test]
#[should_panic = "Cannot add a trailing slash redirect route for `/`"] #[should_panic = "Cannot add a trailing slash redirect route for `/`"]
fn tsr_at_root() { fn tsr_at_root() {
+19 -11
View File
@@ -19,13 +19,13 @@ use axum::{
/// .create(|| async {}) /// .create(|| async {})
/// // `GET /users/new` /// // `GET /users/new`
/// .new(|| async {}) /// .new(|| async {})
/// // `GET /users/:users_id` /// // `GET /users/{users_id}`
/// .show(|Path(user_id): Path<u64>| async {}) /// .show(|Path(user_id): Path<u64>| async {})
/// // `GET /users/:users_id/edit` /// // `GET /users/{users_id}/edit`
/// .edit(|Path(user_id): Path<u64>| async {}) /// .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 {}) /// .update(|Path(user_id): Path<u64>| async {})
/// // `DELETE /users/:users_id` /// // `DELETE /users/{users_id}`
/// .destroy(|Path(user_id): Path<u64>| async {}); /// .destroy(|Path(user_id): Path<u64>| async {});
/// ///
/// let app = Router::new().merge(users); /// let app = Router::new().merge(users);
@@ -82,7 +82,9 @@ where
self.route(&path, get(handler)) 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 pub fn show<H, T>(self, handler: H) -> Self
where where
H: Handler<T, S>, H: Handler<T, S>,
@@ -92,17 +94,21 @@ where
self.route(&path, get(handler)) 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 pub fn edit<H, T>(self, handler: H) -> Self
where where
H: Handler<T, S>, H: Handler<T, S>,
T: 'static, 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)) 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 pub fn update<H, T>(self, handler: H) -> Self
where where
H: Handler<T, S>, 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 pub fn destroy<H, T>(self, handler: H) -> Self
where where
H: Handler<T, S>, H: Handler<T, S>,
@@ -130,7 +138,7 @@ where
} }
fn show_update_destroy_path(&self) -> String { 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 { 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 { mod tests {
#[allow(unused_imports)] #[allow(unused_imports)]
use super::*; use super::*;
use axum::{body::Body, extract::Path, http::Method, Router}; use axum::{body::Body, extract::Path, http::Method};
use http::Request; use http::Request;
use http_body_util::BodyExt; use http_body_util::BodyExt;
use tower::ServiceExt; use tower::ServiceExt;
+47 -18
View File
@@ -19,15 +19,15 @@ use serde::Serialize;
/// RouterExt, // for `Router::typed_*` /// 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)] /// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")] /// #[typed_path("/users/{id}")]
/// struct UsersMember { /// struct UsersMember {
/// id: u32, /// id: u32,
/// } /// }
/// ///
/// // A regular handler function that takes `UsersMember` as the first argument /// // 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. /// // The `TypedPath` must be the first argument to the function.
/// async fn users_show( /// async fn users_show(
@@ -39,7 +39,7 @@ use serde::Serialize;
/// let app = Router::new() /// let app = Router::new()
/// // Add our typed route to the router. /// // 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` /// // first argument is `UsersMember` which implements `TypedPath`
/// .typed_get(users_show) /// .typed_get(users_show)
/// .typed_post(users_create) /// .typed_post(users_create)
@@ -75,7 +75,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath; /// use axum_extra::routing::TypedPath;
/// ///
/// #[derive(TypedPath, Deserialize)] /// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")] /// #[typed_path("/users/{id}")]
/// struct UsersMember { /// struct UsersMember {
/// id: u32, /// id: u32,
/// } /// }
@@ -85,12 +85,12 @@ use serde::Serialize;
/// ///
/// - A `TypedPath` implementation. /// - A `TypedPath` implementation.
/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`], /// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`],
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must /// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
/// also implement [`serde::Deserialize`], unless it's a unit struct. /// also implement [`serde::Deserialize`], unless it's a unit struct.
/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other /// - 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 /// 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 /// [`Display`] implementation for each field must return something that's compatible with its
/// [`Deserialize`] implementation. /// [`Deserialize`] implementation.
/// ///
/// Additionally the macro will verify the captures in the path matches the fields of the struct. /// 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: /// 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; /// use axum_extra::routing::TypedPath;
/// ///
/// #[derive(TypedPath, Deserialize)] /// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id/teams/:team_id")] /// #[typed_path("/users/{id}/teams/{team_id}")]
/// struct UsersMember { /// struct UsersMember {
/// id: u32, /// id: u32,
/// } /// }
@@ -117,7 +117,7 @@ use serde::Serialize;
/// struct UsersCollection; /// struct UsersCollection;
/// ///
/// #[derive(TypedPath, Deserialize)] /// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")] /// #[typed_path("/users/{id}")]
/// struct UsersMember(u32); /// struct UsersMember(u32);
/// ``` /// ```
/// ///
@@ -130,7 +130,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath; /// use axum_extra::routing::TypedPath;
/// ///
/// #[derive(TypedPath, Deserialize)] /// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")] /// #[typed_path("/users/{id}")]
/// struct UsersMember { /// struct UsersMember {
/// id: String, /// id: String,
/// } /// }
@@ -158,7 +158,7 @@ use serde::Serialize;
/// }; /// };
/// ///
/// #[derive(TypedPath, Deserialize)] /// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id", rejection(UsersMemberRejection))] /// #[typed_path("/users/{id}", rejection(UsersMemberRejection))]
/// struct UsersMember { /// struct UsersMember {
/// id: String, /// id: String,
/// } /// }
@@ -215,7 +215,7 @@ use serde::Serialize;
/// [`Deserialize`]: serde::Deserialize /// [`Deserialize`]: serde::Deserialize
/// [`PathRejection`]: axum::extract::rejection::PathRejection /// [`PathRejection`]: axum::extract::rejection::PathRejection
pub trait TypedPath: std::fmt::Display { 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; const PATH: &'static str;
/// Convert the path into a `Uri`. /// 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 /// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
/// given type. /// 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`]. /// implement [`TypedPath`].
/// ///
/// You normally shouldn't have to use this trait directly. /// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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; use serde::Deserialize;
#[derive(TypedPath, Deserialize)] #[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")] #[typed_path("/users/{id}")]
struct UsersShow { struct UsersShow {
id: i32, id: i32,
} }
@@ -434,4 +442,25 @@ mod tests {
assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true&qux=1337"); 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);
}
} }
+27 -9
View File
@@ -1,12 +1,11 @@
//! Extractor and response for typed headers. //! Extractor and response for typed headers.
use axum::{ use axum::{
async_trait,
extract::FromRequestParts, extract::FromRequestParts,
response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
}; };
use headers::{Header, HeaderMapExt}; use headers::{Header, HeaderMapExt};
use http::request::Parts; use http::{request::Parts, StatusCode};
use std::convert::Infallible; use std::convert::Infallible;
/// Extractor and response that works with typed header values from [`headers`]. /// 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; /// # let _: Router = app;
/// ``` /// ```
/// ///
@@ -55,7 +54,6 @@ use std::convert::Infallible;
#[must_use] #[must_use]
pub struct TypedHeader<T>(pub T); pub struct TypedHeader<T>(pub T);
#[async_trait]
impl<T, S> FromRequestParts<S> for TypedHeader<T> impl<T, S> FromRequestParts<S> for TypedHeader<T>
where where
T: Header, T: Header,
@@ -123,6 +121,14 @@ impl TypedHeaderRejection {
pub fn reason(&self) -> &TypedHeaderRejectionReason { pub fn reason(&self) -> &TypedHeaderRejectionReason {
&self.reason &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`] /// Additional information regarding a [`TypedHeaderRejection`]
@@ -136,9 +142,22 @@ pub enum TypedHeaderRejectionReason {
Error(headers::Error), 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 { impl IntoResponse for TypedHeaderRejection {
fn into_response(self) -> Response { 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 { mod tests {
use super::*; use super::*;
use crate::test_helpers::*; use crate::test_helpers::*;
use axum::{response::IntoResponse, routing::get, Router}; use axum::{routing::get, Router};
#[tokio::test] #[tokio::test]
async fn typed_header() { async fn typed_header() {
@@ -190,7 +209,6 @@ mod tests {
.header("user-agent", "foobar") .header("user-agent", "foobar")
.header("cookie", "a=1; b=2") .header("cookie", "a=1; b=2")
.header("cookie", "c=3") .header("cookie", "c=3")
.send()
.await; .await;
let body = res.text().await; let body = res.text().await;
assert_eq!( assert_eq!(
@@ -198,11 +216,11 @@ mod tests {
r#"User-Agent="foobar", Cookie=[("a", "1"), ("b", "2"), ("c", "3")]"# 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; let body = res.text().await;
assert_eq!(body, r#"User-Agent="foobar", Cookie=[]"#); 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; let body = res.text().await;
assert_eq!(body, "Header of type `user-agent` was missing"); assert_eq!(body, "Header of type `user-agent` was missing");
} }
+20 -2
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/), 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). 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) # 0.4.0 (27. November, 2023)
+4 -6
View File
@@ -2,14 +2,14 @@
categories = ["asynchronous", "network-programming", "web-programming"] categories = ["asynchronous", "network-programming", "web-programming"]
description = "Macros for axum" description = "Macros for axum"
edition = "2021" edition = "2021"
rust-version = "1.66" rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum" homepage = "https://github.com/tokio-rs/axum"
keywords = ["axum"] keywords = ["axum"]
license = "MIT" license = "MIT"
name = "axum-macros" name = "axum-macros"
readme = "README.md" readme = "README.md"
repository = "https://github.com/tokio-rs/axum" 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] [features]
default = [] default = []
@@ -19,7 +19,6 @@ __private = ["syn/visit-mut"]
proc-macro = true proc-macro = true
[dependencies] [dependencies]
heck = "0.4"
proc-macro2 = "1.0" proc-macro2 = "1.0"
quote = "1.0" quote = "1.0"
syn = { version = "2.0", features = [ syn = { version = "2.0", features = [
@@ -30,8 +29,8 @@ syn = { version = "2.0", features = [
] } ] }
[dev-dependencies] [dev-dependencies]
axum = { path = "../axum", version = "0.7.2", features = ["macros"] } axum = { path = "../axum", features = ["macros"] }
axum-extra = { path = "../axum-extra", version = "0.9.0", features = ["typed-routing", "cookie-private", "typed-header"] } axum-extra = { path = "../axum-extra", features = ["typed-routing", "cookie-private", "typed-header"] }
rustversion = "1.0" rustversion = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
@@ -44,4 +43,3 @@ allowed = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"]
+1 -1
View File
@@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version ## Minimum supported Rust version
axum-macros's MSRV is 1.66. axum-macros's MSRV is 1.75.
## Getting Help ## Getting Help
+1 -1
View File
@@ -1 +1 @@
nightly-2023-09-23 nightly-2024-06-22
+426 -129
View File
@@ -1,21 +1,26 @@
use std::collections::HashSet; use std::{collections::HashSet, fmt};
use crate::{ use crate::{
attr_parsing::{parse_assignment_attribute, second}, attr_parsing::{parse_assignment_attribute, second},
with_position::{Position, WithPosition}, with_position::{Position, WithPosition},
}; };
use proc_macro2::{Span, TokenStream}; use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned}; 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 Attrs { state_ty } = attr;
let mut state_ty = state_ty.map(second); let mut state_ty = state_ty.map(second);
let check_extractor_count = check_extractor_count(&item_fn); let check_extractor_count = check_extractor_count(&item_fn, kind);
let check_path_extractor = check_path_extractor(&item_fn); let check_path_extractor = check_path_extractor(&item_fn, kind);
let check_output_impls_into_response = check_output_impls_into_response(&item_fn); 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 // 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. // 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( err = Some(
syn::Error::new( syn::Error::new(
Span::call_site(), Span::call_site(),
"can't infer state type, please add set it explicitly, as in \ format!(
`#[debug_handler(state = MyStateType)]`", "can't infer state type, please add set it explicitly, as in \
`#[axum_macros::debug_{kind}(state = MyStateType)]`"
),
) )
.into_compile_error(), .into_compile_error(),
); );
@@ -43,16 +50,16 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
err.unwrap_or_else(|| { err.unwrap_or_else(|| {
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(())); 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! { quote! {
#check_input_order #check_input_order
#check_future_send #check_future_send
} }
} else { } else {
let check_inputs_impls_from_request = 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! { quote! {
#check_inputs_impls_from_request #check_inputs_impls_from_request
@@ -63,17 +70,45 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
} else { } else {
syn::Error::new_spanned( syn::Error::new_spanned(
&item_fn.sig.generics, &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() .into_compile_error()
}; };
let middleware_takes_next_as_last_arg =
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
quote! { quote! {
#item_fn #item_fn
#check_extractor_count #check_extractor_count
#check_path_extractor #check_path_extractor
#check_output_impls_into_response #check_output_impls_into_response
#check_inputs_and_future_send #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; 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 None
} else { } else {
let error_message = format!( 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", 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(); let error = syn::Error::new_spanned(&item_fn.sig.inputs, error_message).to_compile_error();
Some(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 item_fn
.sig .sig
.inputs .inputs
.iter() .iter()
.filter(move |arg| skip_next_arg(arg, kind))
.enumerate() .enumerate()
.filter_map(|(idx, fn_arg)| match fn_arg { .filter_map(|(idx, fn_arg)| match fn_arg {
FnArg::Receiver(_) => None, 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 { fn check_path_extractor(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
let path_extractors = extractor_idents(item_fn) let path_extractors = extractor_idents(item_fn, kind)
.filter(|(_, _, ident)| *ident == "Path") .filter(|(_, _, ident)| *ident == "Path")
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -174,121 +220,294 @@ fn is_self_pat_type(typed: &syn::PatType) -> bool {
ident == "self" ident == "self"
} }
fn check_inputs_impls_from_request(item_fn: &ItemFn, state_ty: Type) -> TokenStream { fn check_inputs_impls_from_request(
let takes_self = item_fn.sig.inputs.first().map_or(false, |arg| match arg { 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::Receiver(_) => true,
FnArg::Typed(typed) => is_self_pat_type(typed), FnArg::Typed(typed) => is_self_pat_type(typed),
}); });
WithPosition::new(item_fn.sig.inputs.iter()) WithPosition::new(
.enumerate() item_fn
.map(|(idx, arg)| { .sig
let must_impl_from_request_parts = match &arg { .inputs
Position::First(_) | Position::Middle(_) => true, .iter()
Position::Last(_) | Position::Only(_) => false, .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 { let (span, ty) = match arg {
FnArg::Receiver(receiver) => { FnArg::Receiver(receiver) => {
if receiver.reference.is_some() { if receiver.reference.is_some() {
return syn::Error::new_spanned( return syn::Error::new_spanned(
receiver, receiver,
"Handlers must only take owned values", "Handlers must only take owned values",
) )
.into_compile_error(); .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)) (span, syn::parse_quote!(Self))
} } else {
FnArg::Typed(typed) => { (span, ty.clone())
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
} }
} }
}) };
.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 let types_that_consume_the_request = item_fn
.sig .sig
.inputs .inputs
.iter() .iter()
.filter(|arg| skip_next_arg(arg, kind))
.enumerate() .enumerate()
.filter_map(|(idx, arg)| { .filter_map(|(idx, arg)| {
let ty = match 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 // exactly one type that consumes the request
if types_that_consume_the_request.len() == 1 { if types_that_consume_the_request.len() == 1 {
// and that is not the last // 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 (_idx, type_name, span) = &types_that_consume_the_request[0];
let error = format!( let error = format!(
"`{type_name}` consumes the request body and thus must be \ "`{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); compile_error!(#error);
}) })
} else { } 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 { .map(|pos| match pos {
Position::First((_, type_name, _)) | Position::Middle((_, type_name, _)) => { Position::First((_, type_name, _)) | Position::Middle((_, type_name, _)) => {
format!("`{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 { let path = match ty {
Type::Path(type_path) => &type_path.path, Type::Path(type_path) => &type_path.path,
_ => return None, _ => return None,
}; };
path.segments.last().map(|p| p.ident.to_string())
}
let ident = match path.segments.last() { fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
Some(path_segment) => &path_segment.ident, let typename = extract_clean_typename(ty)?;
None => return None,
};
let type_name = match &*ident.to_string() { let type_name = match &*typename {
"Json" => "Json<_>", "Json" => "Json<_>",
"RawBody" => "RawBody<_>", "RawBody" => "RawBody<_>",
"RawForm" => "RawForm", "RawForm" => "RawForm",
@@ -384,6 +603,22 @@ fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
Some(type_name) 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 { fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
let ty = match &item_fn.sig.output { let ty = match &item_fn.sig.output {
syn::ReturnType::Default => return quote! {}, 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() { if item_fn.sig.asyncness.is_none() {
match &item_fn.sig.output { match &item_fn.sig.output {
syn::ReturnType::Default => { syn::ReturnType::Default => {
return syn::Error::new_spanned( return syn::Error::new_spanned(
item_fn.sig.fn_token, item_fn.sig.fn_token,
"Handlers must be `async fn`s", format!("{} must be `async fn`s", kind.name_uppercase_plural()),
) )
.into_compile_error(); .into_compile_error();
} }
@@ -583,7 +818,69 @@ fn state_types_from_args(item_fn: &ItemFn) -> HashSet<Type> {
crate::infer_state_types(types).collect() 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] #[test]
fn ui() { fn ui_debug_handler() {
crate::run_ui_tests("debug_handler"); crate::run_ui_tests("debug_handler");
} }
#[test]
fn ui_debug_middleware() {
crate::run_ui_tests("debug_middleware");
}
+1 -1
View File
@@ -54,7 +54,7 @@ fn expand_field(state: &Ident, idx: usize, field: &Field) -> TokenStream {
}; };
quote_spanned! {span=> 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 { impl ::axum::extract::FromRef<#state> for #field_ty {
fn from_ref(state: &#state) -> Self { fn from_ref(state: &#state) -> Self {
#body #body
+43 -30
View File
@@ -180,7 +180,7 @@ pub(crate) fn expand(item: syn::Item, tr: Trait) -> syn::Result<TokenStream> {
variants, variants,
} = item; } = 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() { if !generics.params.is_empty() {
return Err(syn::Error::new_spanned(generics, generics_error)); 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(); let field = fields_unnamed.unnamed.first().unwrap();
if let syn::Type::Path(type_path) = &field.ty { if let syn::Type::Path(type_path) = &field.ty {
if type_path if type_path.path.get_ident() != Some(ty_ident) {
.path
.get_ident()
.map_or(true, |field_type_ident| field_type_ident != ty_ident)
{
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
type_path, type_path,
format_args!( format_args!(
@@ -373,7 +369,6 @@ fn impl_struct_by_extracting_each_field(
Ok(match tr { Ok(match tr {
Trait::FromRequest => quote! { Trait::FromRequest => quote! {
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident
where where
@@ -390,7 +385,6 @@ fn impl_struct_by_extracting_each_field(
} }
}, },
Trait::FromRequestParts => quote! { Trait::FromRequestParts => quote! {
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident
where 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 { if let Some((_, path)) = via {
let span = path.span(); let span = path.span();
quote_spanned! {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 mut fields_iter = fields.iter();
let last = match tr { let last = match tr {
@@ -464,16 +475,17 @@ fn extract_fields(
let member = member(field, index); let member = member(field, index);
let ty_span = field.ty.span(); 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() { if peel_option(&field.ty).is_some() {
let field_ty = into_outer(&via, ty_span, peel_option(&field.ty).unwrap());
let tokens = match tr { let tokens = match tr {
Trait::FromRequest => { Trait::FromRequest => {
quote_spanned! {ty_span=> quote_spanned! {ty_span=>
#member: { #member: {
let (mut parts, body) = req.into_parts(); let (mut parts, body) = req.into_parts();
let value = let value =
::axum::extract::FromRequestParts::from_request_parts( <#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
&mut parts, &mut parts,
state, state,
) )
@@ -488,7 +500,7 @@ fn extract_fields(
Trait::FromRequestParts => { Trait::FromRequestParts => {
quote_spanned! {ty_span=> quote_spanned! {ty_span=>
#member: { #member: {
::axum::extract::FromRequestParts::from_request_parts( <#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
parts, parts,
state, state,
) )
@@ -501,13 +513,14 @@ fn extract_fields(
}; };
Ok(tokens) Ok(tokens)
} else if peel_result_ok(&field.ty).is_some() { } 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 { let tokens = match tr {
Trait::FromRequest => { Trait::FromRequest => {
quote_spanned! {ty_span=> quote_spanned! {ty_span=>
#member: { #member: {
let (mut parts, body) = req.into_parts(); let (mut parts, body) = req.into_parts();
let value = let value =
::axum::extract::FromRequestParts::from_request_parts( <#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
&mut parts, &mut parts,
state, state,
) )
@@ -521,7 +534,7 @@ fn extract_fields(
Trait::FromRequestParts => { Trait::FromRequestParts => {
quote_spanned! {ty_span=> quote_spanned! {ty_span=>
#member: { #member: {
::axum::extract::FromRequestParts::from_request_parts( <#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
parts, parts,
state, state,
) )
@@ -533,6 +546,7 @@ fn extract_fields(
}; };
Ok(tokens) Ok(tokens)
} else { } else {
let field_ty = into_outer(&via,ty_span,&field.ty);
let map_err = if let Some(rejection) = rejection { let map_err = if let Some(rejection) = rejection {
quote! { <#rejection as ::std::convert::From<_>>::from } quote! { <#rejection as ::std::convert::From<_>>::from }
} else { } else {
@@ -545,7 +559,7 @@ fn extract_fields(
#member: { #member: {
let (mut parts, body) = req.into_parts(); let (mut parts, body) = req.into_parts();
let value = let value =
::axum::extract::FromRequestParts::from_request_parts( <#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
&mut parts, &mut parts,
state, state,
) )
@@ -560,7 +574,7 @@ fn extract_fields(
Trait::FromRequestParts => { Trait::FromRequestParts => {
quote_spanned! {ty_span=> quote_spanned! {ty_span=>
#member: { #member: {
::axum::extract::FromRequestParts::from_request_parts( <#field_ty as ::axum::extract::FromRequestParts<_>>::from_request_parts(
parts, parts,
state, state,
) )
@@ -582,26 +596,29 @@ fn extract_fields(
let member = member(field, fields.len() - 1); let member = member(field, fields.len() - 1);
let ty_span = field.ty.span(); 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 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=> quote_spanned! {ty_span=>
#member: { #member: {
::axum::extract::FromRequest::from_request(req, state) <#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await .await
.ok() .ok()
.map(#into_inner) .map(#into_inner)
}, },
} }
} else if peel_result_ok(&field.ty).is_some() { } 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=> quote_spanned! {ty_span=>
#member: { #member: {
::axum::extract::FromRequest::from_request(req, state) <#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await .await
.map(#into_inner) .map(#into_inner)
}, },
} }
} else { } else {
let field_ty = into_outer(&via, ty_span, &field.ty);
let map_err = if let Some(rejection) = rejection { let map_err = if let Some(rejection) = rejection {
quote! { <#rejection as ::std::convert::From<_>>::from } quote! { <#rejection as ::std::convert::From<_>>::from }
} else { } else {
@@ -610,7 +627,7 @@ fn extract_fields(
quote_spanned! {ty_span=> quote_spanned! {ty_span=>
#member: { #member: {
::axum::extract::FromRequest::from_request(req, state) <#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await .await
.map(#into_inner) .map(#into_inner)
.map_err(#map_err)? .map_err(#map_err)?
@@ -807,7 +824,6 @@ fn impl_struct_by_extracting_all_at_once(
let tokens = match tr { let tokens = match tr {
Trait::FromRequest => { Trait::FromRequest => {
quote_spanned! {path_span=> quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident #ident_generics impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident #ident_generics
where where
@@ -821,7 +837,7 @@ fn impl_struct_by_extracting_all_at_once(
req: ::axum::http::Request<::axum::body::Body>, req: ::axum::http::Request<::axum::body::Body>,
state: &#state, state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> { ) -> ::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 .await
.map(|#via_path(value)| #value_to_self) .map(|#via_path(value)| #value_to_self)
.map_err(#map_err) .map_err(#map_err)
@@ -831,7 +847,6 @@ fn impl_struct_by_extracting_all_at_once(
} }
Trait::FromRequestParts => { Trait::FromRequestParts => {
quote_spanned! {path_span=> quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident #ident_generics impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident #ident_generics
where where
@@ -845,7 +860,7 @@ fn impl_struct_by_extracting_all_at_once(
parts: &mut ::axum::http::request::Parts, parts: &mut ::axum::http::request::Parts,
state: &#state, state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> { ) -> ::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 .await
.map(|#via_path(value)| #value_to_self) .map(|#via_path(value)| #value_to_self)
.map_err(#map_err) .map_err(#map_err)
@@ -920,7 +935,6 @@ fn impl_enum_by_extracting_all_at_once(
let tokens = match tr { let tokens = match tr {
Trait::FromRequest => { Trait::FromRequest => {
quote_spanned! {path_span=> quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident impl<#impl_generics> ::axum::extract::FromRequest<#trait_generics> for #ident
where where
@@ -932,7 +946,7 @@ fn impl_enum_by_extracting_all_at_once(
req: ::axum::http::Request<::axum::body::Body>, req: ::axum::http::Request<::axum::body::Body>,
state: &#state, state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> { ) -> ::std::result::Result<Self, Self::Rejection> {
::axum::extract::FromRequest::from_request(req, state) <#path::<#ident> as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await .await
.map(|#path(inner)| inner) .map(|#path(inner)| inner)
.map_err(#map_err) .map_err(#map_err)
@@ -942,7 +956,6 @@ fn impl_enum_by_extracting_all_at_once(
} }
Trait::FromRequestParts => { Trait::FromRequestParts => {
quote_spanned! {path_span=> quote_spanned! {path_span=>
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident impl<#impl_generics> ::axum::extract::FromRequestParts<#trait_generics> for #ident
where where
@@ -954,7 +967,7 @@ fn impl_enum_by_extracting_all_at_once(
parts: &mut ::axum::http::request::Parts, parts: &mut ::axum::http::request::Parts,
state: &#state, state: &#state,
) -> ::std::result::Result<Self, Self::Rejection> { ) -> ::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 .await
.map(|#path(inner)| inner) .map(|#path(inner)| inner)
.map_err(#map_err) .map_err(#map_err)
@@ -1003,7 +1016,7 @@ fn infer_state_type_from_field_attributes(fields: &Fields) -> impl Iterator<Item
match fields { match fields {
Fields::Named(fields_named) => { Fields::Named(fields_named) => {
Box::new(fields_named.named.iter().filter_map(|field| { 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 // ideally we should parse things once and pass the data down
let FromRequestFieldAttrs { via } = let FromRequestFieldAttrs { via } =
parse_attrs("from_request", &field.attrs).ok()?; 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) => { Fields::Unnamed(fields_unnamed) => {
Box::new(fields_unnamed.unnamed.iter().filter_map(|field| { 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 // ideally we should parse things once and pass the data down
let FromRequestFieldAttrs { via } = let FromRequestFieldAttrs { via } =
parse_attrs("from_request", &field.attrs).ok()?; parse_attrs("from_request", &field.attrs).ok()?;
+111 -7
View File
@@ -15,7 +15,6 @@
clippy::needless_borrow, clippy::needless_borrow,
clippy::match_wildcard_for_single_variants, clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex, clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock, clippy::await_holding_lock,
clippy::match_on_vec_items, clippy::match_on_vec_items,
clippy::imprecise_flops, clippy::imprecise_flops,
@@ -44,6 +43,7 @@
#![cfg_attr(test, allow(clippy::float_cmp))] #![cfg_attr(test, allow(clippy::float_cmp))]
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))] #![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
use debug_handler::FunctionKind;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{parse::Parse, Type}; 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 /// # The whole type at once
/// ///
/// By using `#[from_request(via(...))]` on the container you can extract 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 /// # 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 /// ```compile_fail
/// #[derive(axum_macros::FromRequest)] /// #[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)) 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: /// 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. /// As the error message says, handler function needs to be async.
/// ///
/// ``` /// ```no_run
/// use axum::{routing::get, Router, debug_handler}; /// use axum::{routing::get, Router, debug_handler};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() { /// async fn main() {
/// # async {
/// let app = Router::new().route("/", get(handler)); /// let app = Router::new().route("/", get(handler));
/// ///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); /// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap(); /// axum::serve(listener, app).await.unwrap();
/// # };
/// } /// }
/// ///
/// #[debug_handler] /// #[debug_handler]
@@ -569,7 +615,65 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
return input; return input;
#[cfg(debug_assertions)] #[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! /// Private API: Do no use this!
+6 -5
View File
@@ -133,7 +133,6 @@ fn expand_named_fields(
let map_err_rejection = map_err_rejection(&rejection); let map_err_rejection = map_err_rejection(&rejection);
let from_request_impl = quote! { let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident impl<S> ::axum::extract::FromRequestParts<S> for #ident
where where
@@ -238,7 +237,6 @@ fn expand_unnamed_fields(
let map_err_rejection = map_err_rejection(&rejection); let map_err_rejection = map_err_rejection(&rejection);
let from_request_impl = quote! { let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident impl<S> ::axum::extract::FromRequestParts<S> for #ident
where where
@@ -322,7 +320,6 @@ fn expand_unit_fields(
}; };
let from_request_impl = quote! { let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived] #[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident impl<S> ::axum::extract::FromRequestParts<S> for #ident
where where
@@ -386,8 +383,12 @@ fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
.split('/') .split('/')
.map(|segment| { .map(|segment| {
if let Some(capture) = segment if let Some(capture) = segment
.strip_prefix(':') .strip_prefix('{')
.or_else(|| segment.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())) Ok(Segment::Capture(capture.to_owned(), path.span()))
} else { } else {
+2 -2
View File
@@ -40,10 +40,10 @@ impl<I> WithPosition<I>
where where
I: Iterator, I: Iterator,
{ {
pub(crate) fn new(iter: I) -> WithPosition<I> { pub(crate) fn new(iter: impl IntoIterator<IntoIter = I>) -> WithPosition<I> {
WithPosition { WithPosition {
handled_first: false, handled_first: false,
peekable: iter.fuse().peekable(), peekable: iter.into_iter().fuse().peekable(),
} }
} }
} }
@@ -1,4 +1,3 @@
#![feature(diagnostic_namespace)]
use axum_macros::debug_handler; use axum_macros::debug_handler;
#[debug_handler] #[debug_handler]
@@ -1,24 +1,24 @@
error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied error[E0277]: the trait bound `bool: FromRequest<(), axum_core::extract::private::ViaParts>` is not satisfied
--> 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) {}
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool` | ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`, which is required by `bool: FromRequest<(), _>`
| |
= note: Function argument is not a valid axum extractor. = note: Function argument is not a valid axum extractor.
See `https://docs.rs/axum/0.7/axum/extract/index.html` for details See `https://docs.rs/axum/0.7/axum/extract/index.html` for details
= help: the following other types implement trait `FromRequestParts<S>`: = help: the following other types implement trait `FromRequestParts<S>`:
<HeaderMap as FromRequestParts<S>> `()` implements `FromRequestParts<S>`
<Extension<T> as FromRequestParts<S>> `(T1, T2)` implements `FromRequestParts<S>`
<Method as FromRequestParts<S>> `(T1, T2, T3)` implements `FromRequestParts<S>`
<axum::http::request::Parts as FromRequestParts<S>> `(T1, T2, T3, T4)` implements `FromRequestParts<S>`
<Uri as FromRequestParts<S>> `(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
<Version as FromRequestParts<S>> `(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
<Extensions as FromRequestParts<S>> `(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
<ConnectInfo<T> as FromRequestParts<S>> `(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
and $N others and $N others
= note: required for `bool` to implement `FromRequest<(), axum_core::extract::private::ViaParts>` = 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` 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` | ^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`
@@ -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() {}
@@ -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;
|
@@ -1,12 +1,8 @@
use axum::{ use axum::extract::{FromRequest, Request};
async_trait,
extract::{Request, FromRequest},
};
use axum_macros::debug_handler; use axum_macros::debug_handler;
struct A; struct A;
#[async_trait]
impl<S> FromRequest<S> for A impl<S> FromRequest<S> for A
where where
S: Send + Sync, S: Send + Sync,
@@ -1,5 +1,5 @@
error: Handlers must only take owned values 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) {}
| ^^^^^^^^^ | ^^^^^^^^^
@@ -1,12 +1,8 @@
use axum::{ use axum::extract::{FromRequest, Request};
async_trait,
extract::{Request, FromRequest},
};
use axum_macros::debug_handler; use axum_macros::debug_handler;
struct A; struct A;
#[async_trait]
impl<S> FromRequest<S> for A impl<S> FromRequest<S> for A
where where
S: Send + Sync, S: Send + Sync,
@@ -1,5 +1,5 @@
error: Handlers must only take owned values 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) {}
| ^^^^^ | ^^^^^
@@ -4,6 +4,6 @@ use axum_macros::debug_handler;
struct Struct {} struct Struct {}
#[debug_handler] #[debug_handler]
async fn handler(foo: Json<Struct>) {} async fn handler(_foo: Json<Struct>) {}
fn main() {} fn main() {}
@@ -1,20 +1,51 @@
error[E0277]: the trait bound `for<'de> Struct: serde::de::Deserialize<'de>` is not satisfied 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>) {} 7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `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>`: = help: the following other types implement trait `serde::de::Deserialize<'de>`:
bool &'a [u8]
char &'a serde_json::raw::RawValue
isize &'a std::path::Path
i8 &'a str
i16 ()
i32 (T,)
i64 (T0, T1)
i128 (T0, T1, T2)
and $N others and $N others
= note: required for `Struct` to implement `serde::de::DeserializeOwned` = note: required for `Struct` to implement `serde::de::DeserializeOwned`
= note: required for `Json<Struct>` to implement `FromRequest<()>` = note: required for `Json<Struct>` to implement `FromRequest<()>`
= help: see issue #48214 = 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`
@@ -1,5 +1,9 @@
use axum::{
body::Bytes,
http::{Method, Uri},
Json,
};
use axum_macros::debug_handler; use axum_macros::debug_handler;
use axum::{Json, body::Bytes, http::{Method, Uri}};
#[debug_handler] #[debug_handler]
async fn one(_: Json<()>, _: String, _: Uri) {} async fn one(_: Json<()>, _: String, _: Uri) {}
@@ -1,11 +1,11 @@
error: Can't have two extractors that consume the request body. `Json<_>` and `String` both do that. 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. 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 --> tests/debug_handler/fail/multiple_request_consumers.rs:12:14
| |
8 | async fn two(_: Json<()>, _: Method, _: Bytes, _: Uri, _: String) {} 12 | async fn two(_: Json<()>, _: Method, _: Bytes, _: Uri, _: String) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -4,7 +4,7 @@ error: future cannot be sent between threads safely
3 | #[debug_handler] 3 | #[debug_handler]
| ^^^^^^^^^^^^^^^^ future returned by `handler` is not `Send` | ^^^^^^^^^^^^^^^^ 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 note: future is not `Send` as this value is used across an await
--> tests/debug_handler/fail/not_send.rs:6:14 --> 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` | --- has type `Rc<()>` which is not `Send`
6 | async {}.await; 6 | async {}.await;
| ^^^^^ await occurs here, with `_rc` maybe used later | ^^^^^ await occurs here, with `_rc` maybe used later
7 | }
| - `_rc` is later dropped here
note: required by a bound in `check` note: required by a bound in `check`
--> tests/debug_handler/fail/not_send.rs:3:1 --> tests/debug_handler/fail/not_send.rs:3:1
| |
@@ -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() {}
@@ -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 | | ) {
| |_^
@@ -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() {}
@@ -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
@@ -0,0 +1,10 @@
#![allow(unused_parens)]
struct NotIntoResponse;
#[axum::debug_handler]
async fn handler() -> (NotIntoResponse) {
panic!()
}
fn main() {}
@@ -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`
@@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::http::Uri; use axum::http::Uri;
use axum_macros::debug_handler;
#[debug_handler] #[debug_handler]
async fn handler( async fn handler(
@@ -20,6 +20,7 @@ async fn handler(
_e15: Uri, _e15: Uri,
_e16: Uri, _e16: Uri,
_e17: Uri, _e17: Uri,
) {} ) {
}
fn main() {} fn main() {}
@@ -1,5 +1,5 @@
use axum::{http::Uri, Json};
use axum_macros::debug_handler; use axum_macros::debug_handler;
use axum::{Json, http::Uri};
#[debug_handler] #[debug_handler]
async fn one(_: Json<()>, _: Uri) {} async fn one(_: Json<()>, _: Uri) {}
@@ -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() {}
@@ -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`
@@ -5,14 +5,14 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
| ^^^^ the trait `IntoResponse` is not implemented for `bool` | ^^^^ the trait `IntoResponse` is not implemented for `bool`
| |
= help: the following other types implement trait `IntoResponse`: = help: the following other types implement trait `IntoResponse`:
Box<str> &'static [u8; N]
Box<[u8]> &'static [u8]
axum::body::Bytes &'static str
Body ()
axum::extract::rejection::FailedToBufferBody (R,)
axum::extract::rejection::LengthLimitError (Response<()>, R)
axum::extract::rejection::UnknownBodyError (Response<()>, T1, R)
axum::extract::rejection::InvalidUtf8 (Response<()>, T1, T2, R)
and $N others and $N others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check` 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 --> tests/debug_handler/fail/wrong_return_type.rs:4:23
@@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum_macros::debug_handler;
#[debug_handler] #[debug_handler]
async fn handler() -> impl IntoResponse { async fn handler() -> impl IntoResponse {
@@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::extract::State; use axum::extract::State;
use axum_macros::debug_handler;
#[debug_handler] #[debug_handler]
async fn handler(_: State<AppState>) {} async fn handler(_: State<AppState>) {}
@@ -8,22 +8,13 @@ async fn handler(_: State<AppState>) {}
async fn handler_2(_: axum::extract::State<AppState>) {} async fn handler_2(_: axum::extract::State<AppState>) {}
#[debug_handler] #[debug_handler]
async fn handler_3( async fn handler_3(_: axum::extract::State<AppState>, _: axum::extract::State<AppState>) {}
_: axum::extract::State<AppState>,
_: axum::extract::State<AppState>,
) {}
#[debug_handler] #[debug_handler]
async fn handler_4( async fn handler_4(_: State<AppState>, _: State<AppState>) {}
_: State<AppState>,
_: State<AppState>,
) {}
#[debug_handler] #[debug_handler]
async fn handler_5( async fn handler_5(_: axum::extract::State<AppState>, _: State<AppState>) {}
_: axum::extract::State<AppState>,
_: State<AppState>,
) {}
#[derive(Clone)] #[derive(Clone)]
struct AppState; struct AppState;
@@ -1,5 +1,5 @@
use axum_macros::debug_handler;
use axum::http::{Method, Uri}; use axum::http::{Method, Uri};
use axum_macros::debug_handler;
#[debug_handler] #[debug_handler]
async fn handler(_one: Method, _two: Uri, _three: String) {} async fn handler(_one: Method, _two: Uri, _three: String) {}
@@ -1,5 +1,5 @@
use axum_macros::debug_handler; use axum_macros::debug_handler;
use std::future::{Ready, ready}; use std::future::{ready, Ready};
#[debug_handler] #[debug_handler]
fn handler() -> Ready<()> { fn handler() -> Ready<()> {
@@ -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; use axum_macros::debug_handler;
fn main() {} fn main() {}
@@ -115,7 +115,6 @@ impl A {
} }
} }
#[async_trait]
impl<S> FromRequestParts<S> for A impl<S> FromRequestParts<S> for A
where where
S: Send + Sync, S: Send + Sync,
@@ -1,12 +1,8 @@
use axum::{ use axum::extract::{FromRequest, Request};
async_trait,
extract::{Request, FromRequest},
};
use axum_macros::debug_handler; use axum_macros::debug_handler;
struct A; struct A;
#[async_trait]
impl<S> FromRequest<S> for A impl<S> FromRequest<S> for A
where where
S: Send + Sync, S: Send + Sync,
@@ -18,7 +14,6 @@ where
} }
} }
#[async_trait]
impl<S> FromRequest<S> for Box<A> impl<S> FromRequest<S> for Box<A>
where where
S: Send + Sync, S: Send + Sync,
@@ -1,6 +1,5 @@
use axum::extract::{FromRef, FromRequest, Request};
use axum_macros::debug_handler; use axum_macros::debug_handler;
use axum::extract::{Request, FromRef, FromRequest};
use axum::async_trait;
#[debug_handler(state = AppState)] #[debug_handler(state = AppState)]
async fn handler(_: A) {} async fn handler(_: A) {}
@@ -10,7 +9,6 @@ struct AppState;
struct A; struct A;
#[async_trait]
impl<S> FromRequest<S> for A impl<S> FromRequest<S> for A
where where
S: Send + Sync, S: Send + Sync,
@@ -1,5 +1,5 @@
use axum::{extract::Request, extract::State};
use axum_macros::debug_handler; use axum_macros::debug_handler;
use axum::{extract::State, extract::Request};
#[debug_handler(state = AppState)] #[debug_handler(state = AppState)]
async fn handler(_: State<AppState>, _: Request) {} async fn handler(_: State<AppState>, _: Request) {}
@@ -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() {}
@@ -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)
@@ -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() {}
@@ -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