mirror of
https://github.com/tokio-rs/axum.git
synced 2026-09-07 00:00:12 +02:00
Merge remote-tracking branch 'origin/main' into david/dont-override-status-codes-of-5xx
This commit is contained in:
+72
-33
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#[rustversion::nightly]
|
|
||||||
fn main() {
|
|
||||||
println!("cargo:rustc-cfg=nightly_error_messages");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustversion::not(nightly)]
|
|
||||||
fn main() {}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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> {
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
nightly-2023-09-23
|
nightly-2024-06-22
|
||||||
|
|||||||
+426
-129
@@ -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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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!
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user