mirror of
https://github.com/tokio-rs/axum.git
synced 2026-02-06 19:28:12 +00:00
Compare commits
174 Commits
axum-v0.8.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dc7a055a9 | ||
|
|
f8e0e6d025 | ||
|
|
7961711fc7 | ||
|
|
b1c5ed99c5 | ||
|
|
5dc504035b | ||
|
|
2df78b7072 | ||
|
|
576968b406 | ||
|
|
81e727faf6 | ||
|
|
3034f2cb16 | ||
|
|
a34d06b3e9 | ||
|
|
c3fc7bb1df | ||
|
|
f829daf094 | ||
|
|
4c09ea7d80 | ||
|
|
309dc56a73 | ||
|
|
204c5adf5f | ||
|
|
02f1dd1731 | ||
|
|
023d8b7e8e | ||
|
|
05de165de2 | ||
|
|
4077966491 | ||
|
|
183ace306a | ||
|
|
051628c163 | ||
|
|
4028d0e7cf | ||
|
|
057cf4a3bf | ||
|
|
0101c2a240 | ||
|
|
f3a95d786a | ||
|
|
370c6df40a | ||
|
|
f72bb26ff3 | ||
|
|
3b9a4193ea | ||
|
|
e3b32f48b5 | ||
|
|
b1cd1c17cb | ||
|
|
d9f79f5616 | ||
|
|
6b0089190c | ||
|
|
cc9f151f3c | ||
|
|
4e2bc8c92a | ||
|
|
f72c298ee8 | ||
|
|
e710a97a5a | ||
|
|
aba8046921 | ||
|
|
adf2e6c6bf | ||
|
|
8eaf49e317 | ||
|
|
5155b9bed7 | ||
|
|
b6ffaee099 | ||
|
|
cae6bc3709 | ||
|
|
061666a111 | ||
|
|
82af1277a4 | ||
|
|
e1ed2f189c | ||
|
|
fd57d871aa | ||
|
|
e2d49c5eee | ||
|
|
d07863f97d | ||
|
|
287c674b65 | ||
|
|
f5804aa6a1 | ||
|
|
f51f3ba436 | ||
|
|
816407a816 | ||
|
|
78656ebb4a | ||
|
|
4b28e4421d | ||
|
|
9795e3be51 | ||
|
|
2059c12868 | ||
|
|
642e4dcb3c | ||
|
|
ca24460fac | ||
|
|
5c4c1658a7 | ||
|
|
b9e35ec780 | ||
|
|
aeff16e91a | ||
|
|
e668598cef | ||
|
|
9bd839e5e9 | ||
|
|
601d775da8 | ||
|
|
7fd17ceba5 | ||
|
|
26367b9f1e | ||
|
|
509016003e | ||
|
|
b1ef45469b | ||
|
|
68320009fb | ||
|
|
c10934c79c | ||
|
|
830a114172 | ||
|
|
3ef6c5d183 | ||
|
|
8954d7922a | ||
|
|
efc1f57b15 | ||
|
|
c4890b844b | ||
|
|
cec605c8eb | ||
|
|
7bc6f52892 | ||
|
|
37a08a17d4 | ||
|
|
8389ab10b0 | ||
|
|
dcbcf5c5fd | ||
|
|
76cb48ebe0 | ||
|
|
4160bbe6de | ||
|
|
f9c0e7069a | ||
|
|
29be91240d | ||
|
|
a847c60937 | ||
|
|
ac4ba7b63a | ||
|
|
86e4442888 | ||
|
|
d8cd5cec4b | ||
|
|
6cea3eadee | ||
|
|
b5fa1428e9 | ||
|
|
858146d1f1 | ||
|
|
f791ec3879 | ||
|
|
72e820f6fa | ||
|
|
8d055800d2 | ||
|
|
bf9b43cabe | ||
|
|
9ed1ad69d2 | ||
|
|
50f0082970 | ||
|
|
cc11b5ad7d | ||
|
|
817aae3d1e | ||
|
|
4d8a6f6f0f | ||
|
|
f09f50c50a | ||
|
|
d184798937 | ||
|
|
40e9d7b22a | ||
|
|
600b16d431 | ||
|
|
e43030d051 | ||
|
|
ab593bbab7 | ||
|
|
1073468163 | ||
|
|
5c090dcb3e | ||
|
|
853d7e707a | ||
|
|
4ab8df5e42 | ||
|
|
94af9c3262 | ||
|
|
d5e505619f | ||
|
|
e8ab6029d1 | ||
|
|
8d0497f793 | ||
|
|
1b844c9fc6 | ||
|
|
83d19e7b4f | ||
|
|
038f0966a8 | ||
|
|
14f15252ee | ||
|
|
490cde6048 | ||
|
|
a0748e5a74 | ||
|
|
25acc8d131 | ||
|
|
e4550d23b1 | ||
|
|
5d5ba01be9 | ||
|
|
64ae48347e | ||
|
|
3fc8e4ae0a | ||
|
|
8f707ca9fd | ||
|
|
86868de80e | ||
|
|
49c8facad3 | ||
|
|
ee1519c82f | ||
|
|
94db5273a5 | ||
|
|
9ec85d6970 | ||
|
|
4b0590b30e | ||
|
|
a57935c039 | ||
|
|
76268ae52b | ||
|
|
dfc15bce9d | ||
|
|
d66cabd5e9 | ||
|
|
7f5cea0e6b | ||
|
|
2977bd4adf | ||
|
|
ff031867df | ||
|
|
3bd445d9ee | ||
|
|
d9609d754f | ||
|
|
0832de443c | ||
|
|
6bc0717b06 | ||
|
|
420d7b6aaa | ||
|
|
1b157a461e | ||
|
|
617594cd4a | ||
|
|
fb64e72de9 | ||
|
|
d7104313cb | ||
|
|
0f255c3c4c | ||
|
|
b8d9a3e764 | ||
|
|
7dc70bf0d3 | ||
|
|
6d78e84398 | ||
|
|
fc14f5c728 | ||
|
|
8202c66a4d | ||
|
|
7eabf7e645 | ||
|
|
9ef53e5a48 | ||
|
|
769e4066b1 | ||
|
|
869ba86e51 | ||
|
|
d0aff24c85 | ||
|
|
dd8d4a47cb | ||
|
|
d72a0f962a | ||
|
|
07f29f6c5c | ||
|
|
7d1dbeb3af | ||
|
|
df55d83f29 | ||
|
|
36f2630e4d | ||
|
|
14e50c0dcb | ||
|
|
f00b14bac6 | ||
|
|
0821a9dc09 | ||
|
|
cd1453f084 | ||
|
|
6ad76dd9a4 | ||
|
|
bf7c5fc5f3 | ||
|
|
64563fb94c | ||
|
|
a9f3172e88 | ||
|
|
ef3a7ccf1c |
297
.github/workflows/CI.yml
vendored
297
.github/workflows/CI.yml
vendored
@ -2,7 +2,7 @@ name: CI
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
MSRV: '1.78'
|
||||
MSRV: '1.80'
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -14,33 +14,37 @@ on:
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
workspace: [".", examples]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@beta
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Check
|
||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
- name: rustfmt
|
||||
run: cargo fmt --all --check
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@beta
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: ${{ matrix.workspace }}
|
||||
- name: Check
|
||||
run: cargo clippy --locked --manifest-path ${{ matrix.workspace }}/Cargo.toml --workspace --all-targets --all-features -- -D warnings
|
||||
- name: rustfmt
|
||||
run: cargo fmt --all --check --manifest-path ${{ matrix.workspace }}/Cargo.toml
|
||||
|
||||
check-docs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: cargo doc -p axum-core
|
||||
run: cargo doc --package axum-core --all-features --no-deps
|
||||
- name: cargo doc -p axum
|
||||
run: cargo doc --package axum --all-features --no-deps
|
||||
- name: cargo doc -p axum-extra
|
||||
run: cargo doc --package axum-extra --all-features --no-deps
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: cargo doc -p axum-core
|
||||
run: cargo doc --package axum-core --all-features --no-deps
|
||||
- name: cargo doc -p axum
|
||||
run: cargo doc --package axum --all-features --no-deps
|
||||
- name: cargo doc -p axum-extra
|
||||
run: cargo doc --package axum-extra --all-features --no-deps
|
||||
env:
|
||||
RUSTDOCFLAGS: "-D rustdoc::all -A rustdoc::private-doc-tests"
|
||||
|
||||
@ -50,33 +54,31 @@ jobs:
|
||||
# Fail the build if there are any warnings
|
||||
RUSTFLAGS: "-D warnings"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install cargo-hack
|
||||
run: |
|
||||
curl -LsSf https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin
|
||||
- name: cargo hack check
|
||||
run: cargo hack check --each-feature --no-dev-deps --all
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- name: cargo hack check
|
||||
run: cargo hack check --each-feature --no-dev-deps --all
|
||||
|
||||
cargo-public-api-crates:
|
||||
external-types:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
crate: [axum, axum-core, axum-extra, axum-macros]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install cargo-public-api-crates
|
||||
run: |
|
||||
cargo install --git https://github.com/jplatte/cargo-public-api-crates
|
||||
- name: cargo public-api-crates check
|
||||
run: cargo public-api-crates --manifest-path ${{ matrix.crate }}/Cargo.toml check
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-10-18
|
||||
- name: Install cargo-check-external-types
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: cargo-check-external-types@0.4.0
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- run: cargo hack --no-private --exclude axum-macros check-external-types --all-features
|
||||
|
||||
test-versions:
|
||||
needs: check
|
||||
@ -84,35 +86,37 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
rust: [stable, beta]
|
||||
workspace: [".", examples]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --all-features --all-targets
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: ${{ matrix.workspace }}
|
||||
- name: Run tests
|
||||
run: cargo test --locked --workspace --all-features --all-targets --manifest-path ${{ matrix.workspace }}/Cargo.toml
|
||||
|
||||
# some examples don't support our MSRV so we only test axum itself on our MSRV
|
||||
test-nightly:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get rust-toolchain version
|
||||
id: rust-toolchain
|
||||
run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ steps.rust-toolchain.outputs.version }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Run nightly tests
|
||||
working-directory: axum-macros
|
||||
run: cargo test
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get rust-toolchain version
|
||||
id: rust-toolchain
|
||||
run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ steps.rust-toolchain.outputs.version }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Run nightly tests
|
||||
working-directory: axum-macros
|
||||
run: cargo test
|
||||
|
||||
# some examples don't support our MSRV (such as async-graphql)
|
||||
# so we only test axum itself on our MSRV
|
||||
@ -120,51 +124,25 @@ jobs:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.MSRV }}
|
||||
- name: "install Rust nightly"
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Select minimal version
|
||||
run: cargo +nightly update -Z minimal-versions
|
||||
- name: Fix up Cargo.lock
|
||||
run: cargo +nightly update -p crc32fast --precise 1.1.1
|
||||
- name: Run tests
|
||||
run: >
|
||||
cargo +${{ env.MSRV }}
|
||||
test
|
||||
-p axum
|
||||
-p axum-extra
|
||||
-p axum-core
|
||||
--all-features
|
||||
--locked
|
||||
# the compiler errors are different on our MSRV which makes
|
||||
# the trybuild tests in axum-macros fail, so just run the doc
|
||||
# tests
|
||||
- name: Run axum-macros doc tests
|
||||
run: >
|
||||
cargo +${{ env.MSRV }}
|
||||
test
|
||||
-p axum-macros
|
||||
--doc
|
||||
--all-features
|
||||
--locked
|
||||
|
||||
test-docs:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Run doc tests
|
||||
run: cargo test --all-features --doc
|
||||
- uses: actions/checkout@v6
|
||||
- name: "install Rust nightly"
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Select minimal version
|
||||
run: cargo +nightly update -Z minimal-versions
|
||||
- name: Fix up Cargo.lock - crc32fast
|
||||
run: cargo +nightly update -p crc32fast --precise 1.1.1
|
||||
- name: Fix up Cargo.lock - version_check
|
||||
run : cargo +nightly update -p version_check@0.1.0 --precise 0.1.4
|
||||
- name: Fix up Cargo.lock - lazy_static
|
||||
run: cargo +nightly update -p lazy_static --precise 1.1.0
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.MSRV }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Run tests
|
||||
run: cargo +${{ env.MSRV }} test --locked --all-features
|
||||
|
||||
deny-check:
|
||||
name: cargo-deny check
|
||||
@ -176,7 +154,7 @@ jobs:
|
||||
- advisories
|
||||
- bans licenses sources
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
@ -186,71 +164,56 @@ jobs:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: armv5te-unknown-linux-musleabi
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Check
|
||||
env:
|
||||
# Clang has native cross-compilation support
|
||||
CC: clang
|
||||
run: >
|
||||
cargo
|
||||
check
|
||||
--all-targets
|
||||
--all-features
|
||||
-p axum
|
||||
-p axum-core
|
||||
-p axum-extra
|
||||
-p axum-macros
|
||||
--target armv5te-unknown-linux-musleabi
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: armv5te-unknown-linux-musleabi
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Check
|
||||
env:
|
||||
# Clang has native cross-compilation support
|
||||
CC: clang
|
||||
run: cargo check --all-targets --all-features --target armv5te-unknown-linux-musleabi
|
||||
|
||||
wasm32-unknown-unknown:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Check
|
||||
run: >
|
||||
cargo
|
||||
check
|
||||
--manifest-path ./examples/simple-router-wasm/Cargo.toml
|
||||
--target wasm32-unknown-unknown
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: examples
|
||||
- name: Check
|
||||
run: >
|
||||
cargo
|
||||
check
|
||||
--manifest-path ./examples/simple-router-wasm/Cargo.toml
|
||||
--target wasm32-unknown-unknown
|
||||
|
||||
dependencies-are-sorted:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@beta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install cargo-sort
|
||||
run: |
|
||||
cargo install cargo-sort
|
||||
# Work around cargo-sort not honoring workspace.exclude
|
||||
- name: Remove non-crate folder
|
||||
run: rm -rf examples/async-graphql
|
||||
- name: Check dependency tables
|
||||
run: |
|
||||
cargo sort --workspace --grouped --check
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install cargo-sort
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-sort@2.0.2
|
||||
- name: Check dependency tables
|
||||
run: cargo-sort --workspace --grouped --check
|
||||
- name: Check examples dependency tables
|
||||
run: cargo-sort --workspace --grouped --check
|
||||
working-directory: examples
|
||||
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.29.4
|
||||
|
||||
4974
Cargo.lock
generated
4974
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@ -1,13 +1,9 @@
|
||||
[workspace]
|
||||
members = ["axum", "axum-*", "examples/*"]
|
||||
# Only check / build main crates by default (check all with `--workspace`)
|
||||
default-members = ["axum", "axum-*"]
|
||||
# Example has been deleted, but README.md remains
|
||||
exclude = ["examples/async-graphql"]
|
||||
members = ["axum", "axum-*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
@ -22,31 +18,42 @@ type_complexity = "allow"
|
||||
|
||||
await_holding_lock = "warn"
|
||||
dbg_macro = "warn"
|
||||
empty_enum = "warn"
|
||||
empty_enums = "warn"
|
||||
enum_glob_use = "warn"
|
||||
equatable_if_let = "warn"
|
||||
exit = "warn"
|
||||
filter_map_next = "warn"
|
||||
fn_params_excessive_bools = "warn"
|
||||
if_let_mutex = "warn"
|
||||
implicit_clone = "warn"
|
||||
imprecise_flops = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
linkedlist = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
macro_use_imports = "warn"
|
||||
manual_let_else = "warn"
|
||||
match_same_arms = "warn"
|
||||
match_wildcard_for_single_variants = "warn"
|
||||
mem_forget = "warn"
|
||||
must_use_candidate = "warn"
|
||||
needless_borrow = "warn"
|
||||
needless_continue = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_pass_by_value = "warn"
|
||||
option_option = "warn"
|
||||
redundant_clone = "warn"
|
||||
ref_option = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_self_not_must_use = "warn"
|
||||
single_match_else = "warn"
|
||||
str_to_string = "warn"
|
||||
suboptimal_flops = "warn"
|
||||
todo = "warn"
|
||||
trivially_copy_pass_by_ref = "warn"
|
||||
uninlined_format_args = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
unused_self = "warn"
|
||||
use_self = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
|
||||
# configuration for https://github.com/crate-ci/typos
|
||||
|
||||
21
ECOSYSTEM.md
21
ECOSYSTEM.md
@ -24,13 +24,15 @@ If your project isn't listed here and you would like it to be, please feel free
|
||||
- [axum-template](https://github.com/Altair-Bueno/axum-template): Layers, extractors and template engine wrappers for axum based Web MVC applications
|
||||
- [axum-template](https://github.com/janos-r/axum-template): GraphQL and REST API, SurrealDb, JWT auth, direct error handling, request logs
|
||||
- [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
|
||||
- [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-login](https://docs.rs/axum-login): Session-based user authentication for axum.
|
||||
- [axum-gate](https://docs.rs/axum-gate): JWT-based authentication and role-based authorization for axum (Cookie and Bearer, for monolithic and distributed applications).
|
||||
- [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.
|
||||
- [tower-otel](https://github.com/mattiapenati/tower-otel): OpenTelemetry layer for HTTP/gRPC services with optional axum integration.
|
||||
- [jwt-authorizer](https://crates.io/crates/jwt-authorizer): JWT authorization layer for axum (oidc discovery, validation options, claims extraction, etc.)
|
||||
- [axum-typed-multipart](https://crates.io/crates/axum_typed_multipart): Type safe wrapper for `axum::extract::Multipart`.
|
||||
- [tower-governor](https://crates.io/crates/tower_governor): A Tower service and layer that provides a rate-limiting backend by [governor](https://crates.io/crates/governor)
|
||||
@ -41,7 +43,6 @@ If your project isn't listed here and you would like it to be, please feel free
|
||||
- [axum-prometheus](https://github.com/ptrskay3/axum-prometheus): A middleware library to collect HTTP metrics for axum applications, compatible with all [metrics.rs](https://metrics.rs) exporters.
|
||||
- [axum-valid](https://github.com/gengteng/axum-valid): Extractors for data validation using validator, garde, and validify.
|
||||
- [tower-sessions](https://github.com/maxcountryman/tower-sessions): Sessions as a `tower` and `axum` middleware.
|
||||
- [shuttle](https://github.com/shuttle-hq/shuttle): Build & ship backends without writing any infrastructure files. Now with axum support.
|
||||
- [socketioxide](https://github.com/totodore/socketioxide): An easy to use socket.io server implementation working as a `tower` layer/service.
|
||||
- [axum-serde](https://github.com/gengteng/axum-serde): Provides multiple serde-based extractors / responses, also offers a macro to easily customize serde-based extractors / responses.
|
||||
- [loco.rs](https://github.com/loco-rs/loco): A full stack Web and API productivity framework similar to Rails, based on axum.
|
||||
@ -57,6 +58,10 @@ If your project isn't listed here and you would like it to be, please feel free
|
||||
- [baxe](https://github.com/zyphelabs/baxe): Simple macro for defining backend errors once and automatically generate standardized JSON error responses, saving time and reducing complexity
|
||||
- [axum-html-minifier](https://crates.io/crates/axum_html_minifier): This middleware minify the html body content of a axum response.
|
||||
- [static-serve](https://crates.io/crates/static-serve): A helper macro for compressing and embedding static assets in an axum webserver.
|
||||
- [datastar](https://crates.io/crates/datastar): Rust implementation of the Datastar SDK specification with Axum support
|
||||
- [axum-governor](https://crates.io/crates/axum-governor): An independent Axum middleware for rate limiting, powered by [lazy-limit](https://github.com/canmi21/lazy-limit) (not related to tower-governor).
|
||||
- [axum-conditional-requests](https://crates.io/crates/axum-conditional-requests): A library for handling client-side caching HTTP headers
|
||||
- [sigterm](https://github.com/canmi21/sigterm): Signal-aware async control and cancellation primitives for Tokio.
|
||||
|
||||
## Project showcase
|
||||
|
||||
@ -98,6 +103,11 @@ If your project isn't listed here and you would like it to be, please feel free
|
||||
- [sero](https://github.com/clowzed/sero): Host static sites with custom subdomains as surge.sh does. But with full control and cool new features. (axum, sea-orm, postgresql)
|
||||
- [Hatsu](https://github.com/importantimport/hatsu): 🩵 Self-hosted & Fully-automated ActivityPub Bridge for Static Sites.
|
||||
- [Mini RPS](https://github.com/marcodpt/minirps): Mini reverse proxy server, HTTPS, CORS, static file hosting and template engine (minijinja).
|
||||
- [fx](https://github.com/rikhuijzer/fx): A (micro)blogging server that you can self-host.
|
||||
- [clean_axum_demo](https://github.com/sukjaelee/clean_axum_demo): A modern, clean-architecture Rust API server template built with Axum and SQLx. It incorporates domain-driven design, repository patterns, JWT authentication, file uploads, Swagger documentation, OpenTelemetry.
|
||||
- [qiluo-admin](https://github.com/chelunfu/qiluo_admin) | Axum + SeaORM + JWT + Scheduled + Tasks + SnowId + Redis + Memory + VUE3 | DB: MySQL, Postgres, SQLite
|
||||
- [openapi-rs](https://github.com/baerwang/openapi-rs/tree/main/examples/axum) | This project adds a middleware layer to axum using openapi-rs, enabling automatic request validation and processing based on OpenAPI 3.1 specifications. It helps ensure that the server behavior strictly follows the OpenAPI contract.
|
||||
- [axum-rest-api-example](https://github.com/sheroz/axum-rest-api-sample): REST API Web service in Rust using axum, JSON Web Tokens (JWT), SQLx, PostgreSQL, Redis, Docker, structured error handling, and end-to-end API tests.
|
||||
|
||||
[Realworld]: https://github.com/gothinkster/realworld
|
||||
[SQLx]: https://github.com/launchbadge/sqlx
|
||||
@ -106,14 +116,14 @@ If your project isn't listed here and you would like it to be, please feel free
|
||||
|
||||
- [Rust on Nails](https://rust-on-nails.com/): A full stack architecture for Rust web applications
|
||||
- [axum-tutorial] ([website][axum-tutorial-website]): axum tutorial for beginners
|
||||
- [demo-rust-axum]: Demo of Rust and axum web framework
|
||||
- [demo-rust-axum]: Demo of Rust and axum
|
||||
- [Introduction to axum (talk)]: Talk about axum from the Copenhagen Rust Meetup
|
||||
- [Getting Started with Axum]: axum tutorial, GET, POST endpoints and serving files
|
||||
- [Using Rust, Axum, PostgreSQL, and Tokio to build a Blog]
|
||||
- [Introduction to axum]: YouTube playlist
|
||||
- [Rust Axum Full Course]: YouTube video
|
||||
- [Deploying Axum projects with Shuttle]
|
||||
- [API Development with Rust](https://rust-api.dev/docs/front-matter/preface/): REST APIs based on axum
|
||||
- [axum-rest-api-postgres-redis-jwt-docker]: Getting started with REST API Web Services in Rust using Axum, PostgreSQL, Redis, and JWT
|
||||
|
||||
[axum-tutorial]: https://github.com/programatik29/axum-tutorial
|
||||
[axum-tutorial-website]: https://programatik29.github.io/axum-tutorial/
|
||||
@ -123,6 +133,5 @@ If your project isn't listed here and you would like it to be, please feel free
|
||||
[Using Rust, Axum, PostgreSQL, and Tokio to build a Blog]: https://spacedimp.com/blog/using-rust-axum-postgresql-and-tokio-to-build-a-blog/
|
||||
[Introduction to axum]: https://www.youtube.com/playlist?list=PLrmY5pVcnuE-_CP7XZ_44HN-mDrLQV4nS
|
||||
[Rust Axum Full Course]: https://www.youtube.com/watch?v=XZtlD_m59sM
|
||||
[Deploying Axum projects with Shuttle]: https://docs.shuttle.rs/examples/axum
|
||||
|
||||
[axum-rest-api-postgres-redis-jwt-docker]: https://sheroz.com/pages/blog/rust-axum-rest-api-postgres-redis-jwt-docker.html
|
||||
[Building a SaaS with Rust & Next.js](https://joshmo.bearblog.dev/lets-build-a-saas-with-rust/) A tutorial for combining Next.js with Rust via axum to make a SaaS.
|
||||
|
||||
27
LICENSE
Normal file
27
LICENSE
Normal file
@ -0,0 +1,27 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019–2025 axum Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# 0.5.6
|
||||
|
||||
Improve error messages with `#[diagnostic::do_not_recommend]`.
|
||||
|
||||
# 0.5.5
|
||||
|
||||
Released without changes to fix docs.rs build.
|
||||
|
||||
@ -4,12 +4,29 @@ description = "Core types and traits for axum"
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
homepage = "https://github.com/tokio-rs/axum"
|
||||
keywords = ["http", "web", "framework"]
|
||||
keywords = ["http", "web", "routing"]
|
||||
license = "MIT"
|
||||
name = "axum-core"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tokio-rs/axum"
|
||||
version = "0.5.5" # remember to bump the version that axum and axum-extra depend on
|
||||
version = "0.5.6" # remember to bump the version that axum and axum-extra depend on
|
||||
|
||||
[package.metadata.cargo_check_external_types]
|
||||
allowed_external_types = [
|
||||
# not 1.0
|
||||
"futures_core::*",
|
||||
"tower_layer::*",
|
||||
# >=1.0
|
||||
"bytes::*",
|
||||
"http::*",
|
||||
"http_body::*",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["tower-http"] # See __private_docs feature
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing"]
|
||||
@ -38,26 +55,9 @@ axum = { path = "../axum", features = ["__private"] }
|
||||
axum-extra = { path = "../axum-extra", features = ["typed-header"] }
|
||||
axum-macros = { path = "../axum-macros", features = ["__private"] }
|
||||
hyper = "1.0.0"
|
||||
serde = { version = "1.0.200", features = ["derive"] }
|
||||
tokio = { version = "1.25.0", features = ["macros"] }
|
||||
tower-http = { version = "0.6.0", features = ["limit"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.cargo-public-api-crates]
|
||||
allowed = [
|
||||
# not 1.0
|
||||
"futures_core",
|
||||
"tower_layer",
|
||||
|
||||
# >=1.0
|
||||
"bytes",
|
||||
"http",
|
||||
"http_body",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["tower-http"] # See __private_docs feature
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
Copyright 2021 axum Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
axum-core/LICENSE
Symbolic link
1
axum-core/LICENSE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@ -35,6 +35,9 @@ pub trait OptionalFromRequest<S, M = private::ViaRequest>: Sized {
|
||||
) -> impl Future<Output = Result<Option<Self>, Self::Rejection>> + Send;
|
||||
}
|
||||
|
||||
// Compiler hint just says that there is an impl for Option<T>, not mentioning
|
||||
// the bounds, which is not very helpful.
|
||||
#[diagnostic::do_not_recommend]
|
||||
impl<S, T> FromRequestParts<S> for Option<T>
|
||||
where
|
||||
T: OptionalFromRequestParts<S>,
|
||||
@ -42,6 +45,7 @@ where
|
||||
{
|
||||
type Rejection = T::Rejection;
|
||||
|
||||
#[allow(clippy::use_self)]
|
||||
fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &S,
|
||||
@ -50,6 +54,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend]
|
||||
impl<S, T> FromRequest<S> for Option<T>
|
||||
where
|
||||
T: OptionalFromRequest<S>,
|
||||
@ -57,6 +62,7 @@ where
|
||||
{
|
||||
type Rejection = T::Rejection;
|
||||
|
||||
#[allow(clippy::use_self)]
|
||||
async fn from_request(req: Request, state: &S) -> Result<Option<T>, Self::Rejection> {
|
||||
T::from_request(req, state).await
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend] // pretty niche impl
|
||||
impl<S> FromRequest<S> for BytesMut
|
||||
where
|
||||
S: Send + Sync,
|
||||
@ -73,6 +74,7 @@ where
|
||||
|
||||
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
|
||||
let mut body = req.into_limited_body();
|
||||
#[allow(clippy::use_self)]
|
||||
let mut bytes = BytesMut::new();
|
||||
body_to_bytes_mut(&mut body, &mut bytes).await?;
|
||||
Ok(bytes)
|
||||
@ -128,12 +130,14 @@ where
|
||||
}
|
||||
})?;
|
||||
|
||||
#[allow(clippy::use_self)]
|
||||
let string = String::from_utf8(bytes.into()).map_err(InvalidUtf8::from_err)?;
|
||||
|
||||
Ok(string)
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend] // pretty niche impl
|
||||
impl<S> FromRequestParts<S> for Parts
|
||||
where
|
||||
S: Send + Sync,
|
||||
@ -145,6 +149,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend] // pretty niche impl
|
||||
impl<S> FromRequestParts<S> for Extensions
|
||||
where
|
||||
S: Send + Sync,
|
||||
|
||||
@ -3,6 +3,7 @@ use crate::response::{IntoResponse, Response};
|
||||
use http::request::Parts;
|
||||
use std::{convert::Infallible, future::Future};
|
||||
|
||||
#[diagnostic::do_not_recommend]
|
||||
impl<S> FromRequestParts<S> for ()
|
||||
where
|
||||
S: Send + Sync,
|
||||
@ -18,6 +19,7 @@ macro_rules! impl_from_request {
|
||||
(
|
||||
[$($ty:ident),*], $last:ident
|
||||
) => {
|
||||
#[diagnostic::do_not_recommend]
|
||||
#[allow(non_snake_case, unused_mut, unused_variables)]
|
||||
impl<S, $($ty,)* $last> FromRequestParts<S> for ($($ty,)* $last,)
|
||||
where
|
||||
@ -43,6 +45,7 @@ macro_rules! impl_from_request {
|
||||
|
||||
// This impl must not be generic over M, otherwise it would conflict with the blanket
|
||||
// implementation of `FromRequest<S, Mut>` for `T: FromRequestParts<S>`.
|
||||
#[diagnostic::do_not_recommend]
|
||||
#[allow(non_snake_case, unused_mut, unused_variables)]
|
||||
impl<S, $($ty,)* $last> FromRequest<S> for ($($ty,)* $last,)
|
||||
where
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use super::{IntoResponseParts, Response, ResponseParts};
|
||||
use super::{ForceStatusCode, IntoResponseFailed, IntoResponseParts, Response, ResponseParts};
|
||||
use crate::{body::Body, BoxError};
|
||||
use bytes::{buf::Chain, Buf, Bytes, BytesMut};
|
||||
use http::{
|
||||
@ -329,7 +329,9 @@ where
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
let mut res = self.1.into_response();
|
||||
*res.status_mut() = self.0;
|
||||
if res.extensions().get::<IntoResponseFailed>().is_none() {
|
||||
*res.status_mut() = self.0;
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
@ -405,18 +407,16 @@ macro_rules! impl_into_response {
|
||||
let ($($ty),*, res) = self;
|
||||
|
||||
let res = res.into_response();
|
||||
let parts = ResponseParts { res };
|
||||
|
||||
$(
|
||||
let parts = match $ty.into_response_parts(parts) {
|
||||
if res.extensions().get::<IntoResponseFailed>().is_none() {
|
||||
let parts = ResponseParts { res };
|
||||
let parts = match ($($ty,)*).into_response_parts(parts) {
|
||||
Ok(parts) => parts,
|
||||
Err(err) => {
|
||||
return err.into_response();
|
||||
}
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
)*
|
||||
|
||||
parts.res
|
||||
parts.res
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -430,16 +430,40 @@ macro_rules! impl_into_response {
|
||||
let (status, $($ty),*, res) = self;
|
||||
|
||||
let res = res.into_response();
|
||||
let parts = ResponseParts { res };
|
||||
|
||||
$(
|
||||
let parts = match $ty.into_response_parts(parts) {
|
||||
if res.extensions().get::<IntoResponseFailed>().is_none() {
|
||||
let parts = ResponseParts { res };
|
||||
let mut parts = match ($($ty,)*).into_response_parts(parts) {
|
||||
Ok(parts) => parts,
|
||||
Err(err) => {
|
||||
return err.into_response();
|
||||
}
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
)*
|
||||
|
||||
// Don't call `(status, parts.res).into_response()` since that checks for
|
||||
// `IntoResponseFailed` and skips setting the status. We've already done that
|
||||
// check here so overriding the status is required if returning
|
||||
// `(IntoResponseFailed, StatusCode::INTERNAL_SERVER_ERROR)`
|
||||
*parts.res.status_mut() = status;
|
||||
parts.res
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
impl<R, $($ty,)*> IntoResponse for (ForceStatusCode, $($ty),*, R)
|
||||
where
|
||||
$( $ty: IntoResponseParts, )*
|
||||
R: IntoResponse,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
let (status, $($ty),*, res) = self;
|
||||
|
||||
let res = res.into_response();
|
||||
let parts = ResponseParts { res };
|
||||
let parts = match ($($ty,)*).into_response_parts(parts) {
|
||||
Ok(parts) => parts,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
|
||||
(status, parts.res).into_response()
|
||||
}
|
||||
@ -455,17 +479,22 @@ macro_rules! impl_into_response {
|
||||
let (outer_parts, $($ty),*, res) = self;
|
||||
|
||||
let res = res.into_response();
|
||||
let parts = ResponseParts { res };
|
||||
$(
|
||||
let parts = match $ty.into_response_parts(parts) {
|
||||
if res.extensions().get::<IntoResponseFailed>().is_none() {
|
||||
let parts = ResponseParts { res };
|
||||
let mut parts = match ($($ty,)*).into_response_parts(parts) {
|
||||
Ok(parts) => parts,
|
||||
Err(err) => {
|
||||
return err.into_response();
|
||||
}
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
)*
|
||||
|
||||
(outer_parts, parts.res).into_response()
|
||||
// Don't call `(outer_parts, parts.res).into_response()` for the same reason we
|
||||
// don't call `(status, parts.res).into_response()` in the above impl.
|
||||
*parts.res.status_mut() = outer_parts.status;
|
||||
parts.res.headers_mut().extend(outer_parts.headers);
|
||||
parts.res.extensions_mut().extend(outer_parts.extensions);
|
||||
parts.res
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -241,7 +241,9 @@ macro_rules! impl_into_response_parts {
|
||||
let res = match $ty.into_response_parts(res) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
return Err(err.into_response());
|
||||
let mut err_res = err.into_response();
|
||||
err_res.extensions_mut().insert(super::IntoResponseFailed);
|
||||
return Err(err_res);
|
||||
}
|
||||
};
|
||||
)*
|
||||
@ -270,3 +272,19 @@ impl IntoResponseParts for () {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http::StatusCode;
|
||||
|
||||
use crate::response::IntoResponse;
|
||||
|
||||
#[test]
|
||||
fn failed_into_response_parts() {
|
||||
let response = (StatusCode::CREATED, [("\n", "\n")]).into_response();
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
|
||||
let response = (StatusCode::CREATED, [("\n", "\n")], ()).into_response();
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
//!
|
||||
//! [`axum::response`]: https://docs.rs/axum/0.8/axum/response/index.html
|
||||
|
||||
use std::convert::Infallible;
|
||||
|
||||
use http::StatusCode;
|
||||
|
||||
use crate::body::Body;
|
||||
|
||||
mod append_headers;
|
||||
@ -128,3 +132,87 @@ where
|
||||
Self(value.into_response())
|
||||
}
|
||||
}
|
||||
|
||||
/// Response part that stops status code overrides.
|
||||
///
|
||||
/// This type should be used by types implementing [`IntoResponseParts`] or
|
||||
/// [`IntoResponse`] when they fail to produce the response usually expected of
|
||||
/// them and return some sort of error response instead.
|
||||
///
|
||||
/// It is checked used by the tuple impls of [`IntoResponse`] that have a
|
||||
/// [`StatusCode`] as their first element to ignore that status code.
|
||||
/// Consider the following example:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use axum::Json;
|
||||
/// # use http::StatusCode;
|
||||
/// # #[derive(serde::Serialize)]
|
||||
/// # struct CreatedResponse { }
|
||||
/// fn my_handler(/* ... */) -> (StatusCode, Json<CreatedResponse>) {
|
||||
/// // This response type's serialization may fail
|
||||
/// let response = CreatedResponse { /* ... */ };
|
||||
/// (StatusCode::CREATED, Json(response))
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When `response` serialization succeeds, the server responds with a status
|
||||
/// code of 201 Created (overwriting `Json`s default status code of 200 OK),
|
||||
/// and the expected JSON payload.
|
||||
///
|
||||
/// When `response` serialization fails hoewever, `impl IntoResponse for Json`
|
||||
/// return a response with status code 500 Internal Server Error, and
|
||||
/// `IntoResponseFailed` as a response extension, and the 201 Created override
|
||||
/// is ignored.
|
||||
///
|
||||
/// This is a behavior introduced with axum 0.9.\
|
||||
/// To force a status code override even when an inner [`IntoResponseParts`] /
|
||||
/// [`IntoResponse`] failed, use [`ForceStatusCode`].
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct IntoResponseFailed;
|
||||
|
||||
impl IntoResponseParts for IntoResponseFailed {
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
res.extensions_mut().insert(self);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Not sure it makes sense to return `IntoResponseFailed` as the whole response. You should
|
||||
/// probably at least combine it with a status code.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// fn foo()
|
||||
/// where
|
||||
/// axum_core::response::IntoResponseFailed: axum_core::response::IntoResponse,
|
||||
/// {}
|
||||
/// ```
|
||||
#[allow(dead_code)]
|
||||
fn into_response_failed_doesnt_impl_into_response() {}
|
||||
|
||||
/// Set the status code regardless of whether [`IntoResponseFailed`] is used or not.
|
||||
///
|
||||
/// See the docs for [`IntoResponseFailed`] for more details.
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub struct ForceStatusCode(pub StatusCode);
|
||||
|
||||
impl IntoResponse for ForceStatusCode {
|
||||
fn into_response(self) -> Response {
|
||||
let mut res = ().into_response();
|
||||
*res.status_mut() = self.0;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResponse for (ForceStatusCode, R)
|
||||
where
|
||||
R: IntoResponse,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
let (ForceStatusCode(status), res) = self;
|
||||
let mut res = res.into_response();
|
||||
*res.status_mut() = status;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog],
|
||||
and this project adheres to [Semantic Versioning].
|
||||
|
||||
# Unreleased
|
||||
|
||||
- **breaking:** Remove the deprecated `Host`, `Scheme` and `OptionalPath`
|
||||
extractors ([#3599])
|
||||
- Also remove `HostRejection` which only had `FailedToResolveHost`
|
||||
previously used in the `Host` extractor ([#3636])
|
||||
- **breaking:** Change `routing::RouterExt::route_with_tsr` to only redirect
|
||||
the HTTP methods that the supplied `MethodRouter` handles. This allows the
|
||||
following pattern which lead to a panic before because the two
|
||||
`route_with_tsr` calls would both attempt to register a method-independent
|
||||
redirect ([#3586]):
|
||||
|
||||
```rust
|
||||
Router::new()
|
||||
.route_with_tsr("/path", get(/* handler */))
|
||||
.route_with_tsr("/path", post(/* handler */))
|
||||
```
|
||||
|
||||
[#3599]: https://github.com/tokio-rs/axum/pull/3599
|
||||
[#3586]: https://github.com/tokio-rs/axum/pull/3586
|
||||
|
||||
# 0.12.5
|
||||
|
||||
- **fixed:** `JsonLines` now correctly respects the default body limit ([#3591])
|
||||
|
||||
[#3591]: https://github.com/tokio-rs/axum/pull/3591
|
||||
|
||||
# 0.12.4
|
||||
|
||||
- **changed:** Deprecated the `Host` and `Scheme` extractors ([#3595])
|
||||
- Please comment in [#3442] if you depend on one of them
|
||||
|
||||
[#3595]: https://github.com/tokio-rs/axum/pull/3595
|
||||
[#3442]: https://github.com/tokio-rs/axum/issues/3442
|
||||
|
||||
# 0.12.3
|
||||
|
||||
- **changed:** Make the `typed-routing` feature enable the `routing` feature ([#3514])
|
||||
- **changed:** Add trailing newline to `ErasedJson::pretty` response bodies ([#3526])
|
||||
- **fixed:** Fix integer underflow in `FileStream::try_range_response` for empty files ([#3566])
|
||||
|
||||
[#3514]: https://github.com/tokio-rs/axum/pull/3514
|
||||
[#3526]: https://github.com/tokio-rs/axum/pull/3526
|
||||
[#3566]: https://github.com/tokio-rs/axum/pull/3566
|
||||
|
||||
# 0.12.2
|
||||
|
||||
- Make it easier to visually scan for default features ([#3550])
|
||||
|
||||
@ -4,12 +4,35 @@ description = "Extra utilities for axum"
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
homepage = "https://github.com/tokio-rs/axum"
|
||||
keywords = ["http", "web", "framework"]
|
||||
keywords = ["http", "web", "routing"]
|
||||
license = "MIT"
|
||||
name = "axum-extra"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tokio-rs/axum"
|
||||
version = "0.12.2"
|
||||
version = "0.12.5"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[package.metadata.cargo_check_external_types]
|
||||
allowed_external_types = [
|
||||
"axum::*",
|
||||
"axum_core::*",
|
||||
"axum_macros::*",
|
||||
"bytes::*",
|
||||
"cookie::*",
|
||||
"futures_core::*",
|
||||
"futures_util::*",
|
||||
"headers",
|
||||
"headers_core::*",
|
||||
"http::*",
|
||||
"http_body::*",
|
||||
"prost::*",
|
||||
"serde_core::*",
|
||||
"tokio::*",
|
||||
"tower_layer::*",
|
||||
"tower_service::*",
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["tracing"]
|
||||
@ -50,10 +73,8 @@ json-lines = [
|
||||
]
|
||||
middleware = ["dep:axum"]
|
||||
multipart = ["dep:multer", "dep:fastrand"]
|
||||
optional-path = ["dep:axum", "dep:serde_core"]
|
||||
protobuf = ["dep:prost"]
|
||||
routing = ["axum/original-uri", "dep:rustversion"]
|
||||
scheme = []
|
||||
query = [
|
||||
"dep:form_urlencoded",
|
||||
"dep:serde_core",
|
||||
@ -63,6 +84,7 @@ query = [
|
||||
tracing = ["axum-core/tracing", "axum/tracing", "dep:tracing"]
|
||||
typed-header = ["dep:headers"]
|
||||
typed-routing = [
|
||||
"routing",
|
||||
"dep:axum-macros",
|
||||
"dep:percent-encoding",
|
||||
"dep:serde_core",
|
||||
@ -73,10 +95,10 @@ with-rejection = ["dep:axum"]
|
||||
|
||||
# Enabled by docs.rs because it uses all-features
|
||||
# Enables upstream things linked to in docs
|
||||
__private_docs = ["axum/json", "dep:serde", "dep:tower"]
|
||||
__private_docs = ["axum/json", "axum/query", "dep:serde", "dep:tower"]
|
||||
|
||||
[dependencies]
|
||||
axum-core = { path = "../axum-core", version = "0.5.2" }
|
||||
axum-core = { path = "../axum-core", version = "0.5.6" }
|
||||
bytes = "1.1.0"
|
||||
futures-core = "0.3"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||
@ -89,7 +111,7 @@ tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
|
||||
# optional dependencies
|
||||
axum = { path = "../axum", version = "0.8.7", default-features = false, optional = true }
|
||||
axum = { path = "../axum", version = "0.8.8", default-features = false, optional = true }
|
||||
axum-macros = { path = "../axum-macros", version = "0.5.0", optional = true }
|
||||
cookie = { package = "cookie", version = "0.18.0", features = ["percent-encode"], optional = true }
|
||||
fastrand = { version = "2.1.0", optional = true }
|
||||
@ -100,6 +122,11 @@ percent-encoding = { version = "2.1", optional = true }
|
||||
prost = { version = "0.14", optional = true }
|
||||
rustversion = { version = "1.0.9", optional = true }
|
||||
serde_core = { version = "1.0.221", optional = true }
|
||||
# DO NOT update. axum itself uses serde_html_form 0.3.x which has slightly
|
||||
# different behavior in some edge cases. This feature here is kept (deprecated)
|
||||
# to let people transition to that easier (fewer breaking changes to deal with
|
||||
# at once if they keep using axum_extra's `Query` / `Form` initially when going
|
||||
# to axum 0.9).
|
||||
serde_html_form = { version = "0.2.0", optional = true }
|
||||
serde_json = { version = "1.0.71", optional = true }
|
||||
serde_path_to_error = { version = "0.1.8", optional = true }
|
||||
@ -120,13 +147,11 @@ hyper = "1.0.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
|
||||
serde = { version = "1.0.221", features = ["derive"] }
|
||||
serde_json = "1.0.71"
|
||||
tempfile = "3.23.0"
|
||||
tokio = { version = "1.14", features = ["full"] }
|
||||
tower = { version = "0.5.2", features = ["util"] }
|
||||
tower-http = { version = "0.6.0", features = ["map-response-body", "timeout"] }
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-subscriber = "0.3.20"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
Copyright 2021 axum Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
axum-extra/LICENSE
Symbolic link
1
axum-extra/LICENSE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@ -283,8 +283,8 @@ where
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
match self {
|
||||
Either::E1(layer) => Either::E1(layer.layer(inner)),
|
||||
Either::E2(layer) => Either::E2(layer.layer(inner)),
|
||||
Self::E1(layer) => Either::E1(layer.layer(inner)),
|
||||
Self::E2(layer) => Either::E2(layer.layer(inner)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -300,15 +300,15 @@ where
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
match self {
|
||||
Either::E1(inner) => inner.poll_ready(cx),
|
||||
Either::E2(inner) => inner.poll_ready(cx),
|
||||
Self::E1(inner) => inner.poll_ready(cx),
|
||||
Self::E2(inner) => inner.poll_ready(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, req: R) -> Self::Future {
|
||||
match self {
|
||||
Either::E1(inner) => futures_util::future::Either::Left(inner.call(req)),
|
||||
Either::E2(inner) => futures_util::future::Either::Right(inner.call(req)),
|
||||
Self::E1(inner) => futures_util::future::Either::Left(inner.call(req)),
|
||||
Self::E2(inner) => futures_util::future::Either::Right(inner.call(req)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,7 +206,7 @@ impl IntoResponseParts for CookieJar {
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
set_cookies(self.jar, res.headers_mut());
|
||||
set_cookies(&self.jar, res.headers_mut());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
@ -217,7 +217,7 @@ impl IntoResponse for CookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cookies(jar: cookie::CookieJar, headers: &mut HeaderMap) {
|
||||
fn set_cookies(jar: &cookie::CookieJar, headers: &mut HeaderMap) {
|
||||
for cookie in jar.delta() {
|
||||
if let Ok(header_value) = cookie.encoded().to_string().parse() {
|
||||
headers.append(SET_COOKIE, header_value);
|
||||
@ -321,13 +321,13 @@ mod tests {
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for Key {
|
||||
fn from_ref(state: &AppState) -> Key {
|
||||
fn from_ref(state: &AppState) -> Self {
|
||||
state.key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for CustomKey {
|
||||
fn from_ref(state: &AppState) -> CustomKey {
|
||||
fn from_ref(state: &AppState) -> Self {
|
||||
state.custom_key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ where
|
||||
key,
|
||||
_marker: _,
|
||||
} = PrivateCookieJar::from_headers(&parts.headers, key);
|
||||
Ok(PrivateCookieJar {
|
||||
Ok(Self {
|
||||
jar,
|
||||
key,
|
||||
_marker: PhantomData,
|
||||
@ -274,7 +274,7 @@ impl<K> IntoResponseParts for PrivateCookieJar<K> {
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
set_cookies(self.jar, res.headers_mut());
|
||||
set_cookies(&self.jar, res.headers_mut());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,7 +154,7 @@ where
|
||||
key,
|
||||
_marker: _,
|
||||
} = SignedCookieJar::from_headers(&parts.headers, key);
|
||||
Ok(SignedCookieJar {
|
||||
Ok(Self {
|
||||
jar,
|
||||
key,
|
||||
_marker: PhantomData,
|
||||
@ -292,7 +292,7 @@ impl<K> IntoResponseParts for SignedCookieJar<K> {
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
set_cookies(self.jar, res.headers_mut());
|
||||
set_cookies(&self.jar, res.headers_mut());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use axum::extract::rejection::RawFormRejection;
|
||||
use axum::{
|
||||
extract::{FromRequest, RawForm, Request},
|
||||
@ -12,31 +14,16 @@ use serde_core::de::DeserializeOwned;
|
||||
///
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Differences from `axum::extract::Form`
|
||||
/// # Deprecated
|
||||
///
|
||||
/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
|
||||
/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
|
||||
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
|
||||
/// container.
|
||||
/// This extractor used to use a different deserializer under-the-hood but that
|
||||
/// is no longer the case. Now it only uses an older version of the same
|
||||
/// deserializer, purely for ease of transition to the latest version.
|
||||
/// Before switching to `axum::extract::Form`, it is recommended to read the
|
||||
/// [changelog for `serde_html_form v0.3.0`][changelog].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum_extra::extract::Form;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Payload {
|
||||
/// #[serde(rename = "value")]
|
||||
/// values: Vec<String>,
|
||||
/// }
|
||||
///
|
||||
/// async fn accept_form(Form(payload): Form<Payload>) {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`serde_html_form`]: https://crates.io/crates/serde_html_form
|
||||
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
|
||||
#[deprecated = "see documentation"]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[cfg(feature = "form")]
|
||||
pub struct Form<T>(pub T);
|
||||
@ -90,6 +77,7 @@ composite_rejection! {
|
||||
/// Rejection used for [`Form`].
|
||||
///
|
||||
/// Contains one variant for each way the [`Form`] extractor can fail.
|
||||
#[deprecated = "because Form is deprecated"]
|
||||
pub enum FormRejection {
|
||||
RawFormRejection,
|
||||
FailedToDeserializeForm,
|
||||
|
||||
@ -1,246 +0,0 @@
|
||||
use super::rejection::{FailedToResolveHost, HostRejection};
|
||||
use axum_core::{
|
||||
extract::{FromRequestParts, OptionalFromRequestParts},
|
||||
RequestPartsExt,
|
||||
};
|
||||
use http::{
|
||||
header::{HeaderMap, FORWARDED},
|
||||
request::Parts,
|
||||
uri::Authority,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
|
||||
const X_FORWARDED_HOST_HEADER_KEY: &str = "X-Forwarded-Host";
|
||||
|
||||
/// Extractor that resolves the host of the request.
|
||||
///
|
||||
/// Host is resolved through the following, in order:
|
||||
/// - `Forwarded` header
|
||||
/// - `X-Forwarded-Host` header
|
||||
/// - `Host` header
|
||||
/// - Authority of the request URI
|
||||
///
|
||||
/// See <https://www.rfc-editor.org/rfc/rfc9110.html#name-host-and-authority> for the definition of
|
||||
/// host.
|
||||
///
|
||||
/// Note that user agents can set `X-Forwarded-Host` and `Host` headers to arbitrary values so make
|
||||
/// sure to validate them to avoid security issues.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Host(pub String);
|
||||
|
||||
impl<S> FromRequestParts<S> for Host
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = HostRejection;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.extract::<Option<Host>>()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(HostRejection::FailedToResolveHost(FailedToResolveHost))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> OptionalFromRequestParts<S> for Host
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
_state: &S,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
if let Some(host) = parse_forwarded(&parts.headers) {
|
||||
return Ok(Some(Host(host.to_owned())));
|
||||
}
|
||||
|
||||
if let Some(host) = parts
|
||||
.headers
|
||||
.get(X_FORWARDED_HOST_HEADER_KEY)
|
||||
.and_then(|host| host.to_str().ok())
|
||||
{
|
||||
return Ok(Some(Host(host.to_owned())));
|
||||
}
|
||||
|
||||
if let Some(host) = parts
|
||||
.headers
|
||||
.get(http::header::HOST)
|
||||
.and_then(|host| host.to_str().ok())
|
||||
{
|
||||
return Ok(Some(Host(host.to_owned())));
|
||||
}
|
||||
|
||||
if let Some(authority) = parts.uri.authority() {
|
||||
return Ok(Some(Host(parse_authority(authority).to_owned())));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(warnings)]
|
||||
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(',').nth(0)?;
|
||||
|
||||
// find the value of the `host` field
|
||||
first_value.split(';').find_map(|pair| {
|
||||
let (key, value) = pair.split_once('=')?;
|
||||
key.trim()
|
||||
.eq_ignore_ascii_case("host")
|
||||
.then(|| value.trim().trim_matches('"'))
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_authority(auth: &Authority) -> &str {
|
||||
auth.as_str()
|
||||
.rsplit('@')
|
||||
.next()
|
||||
.expect("split always has at least 1 item")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::TestClient;
|
||||
use axum::{routing::get, Router};
|
||||
use http::{header::HeaderName, Request};
|
||||
|
||||
fn test_client() -> TestClient {
|
||||
async fn host_as_body(Host(host): Host) -> String {
|
||||
host
|
||||
}
|
||||
|
||||
TestClient::new(Router::new().route("/", get(host_as_body)))
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn host_header() {
|
||||
let original_host = "some-domain:123";
|
||||
let host = test_client()
|
||||
.get("/")
|
||||
.header(http::header::HOST, original_host)
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(host, original_host);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn x_forwarded_host_header() {
|
||||
let original_host = "some-domain:456";
|
||||
let host = test_client()
|
||||
.get("/")
|
||||
.header(X_FORWARDED_HOST_HEADER_KEY, original_host)
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(host, original_host);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn x_forwarded_host_precedence_over_host_header() {
|
||||
let x_forwarded_host_header = "some-domain:456";
|
||||
let host_header = "some-domain:123";
|
||||
let host = test_client()
|
||||
.get("/")
|
||||
.header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header)
|
||||
.header(http::header::HOST, host_header)
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(host, x_forwarded_host_header);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn uri_host() {
|
||||
let client = test_client();
|
||||
let port = client.server_port();
|
||||
let host = client.get("/").await.text().await;
|
||||
assert_eq!(host, format!("127.0.0.1:{port}"));
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn ip4_uri_host() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap();
|
||||
let host = parts.extract::<Host>().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 = parts.extract::<Host>().await.unwrap();
|
||||
assert_eq!(host.0, "[::1]:456");
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn missing_host() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
let host = parts.extract::<Host>().await.unwrap_err();
|
||||
assert!(matches!(host, HostRejection::FailedToResolveHost(_)));
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn optional_extractor() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap();
|
||||
let host = parts.extract::<Option<Host>>().await.unwrap();
|
||||
assert!(host.is_some());
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn optional_extractor_none() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
let host = parts.extract::<Option<Host>>().await.unwrap();
|
||||
assert!(host.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forwarded_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, "192.0.2.60");
|
||||
|
||||
// is case insensitive
|
||||
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, "192.0.2.60");
|
||||
|
||||
// ipv6
|
||||
let headers = header_map(&[(FORWARDED, "host=\"[2001:db8:cafe::17]:4711\"")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "[2001:db8:cafe::17]:4711");
|
||||
|
||||
// multiple values in one header
|
||||
let headers = header_map(&[(FORWARDED, "host=192.0.2.60, host=127.0.0.1")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "192.0.2.60");
|
||||
|
||||
// multiple header values
|
||||
let headers = header_map(&[
|
||||
(FORWARDED, "host=192.0.2.60"),
|
||||
(FORWARDED, "host=127.0.0.1"),
|
||||
]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "192.0.2.60");
|
||||
}
|
||||
|
||||
fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
for (key, value) in values {
|
||||
headers.append(key, value.parse().unwrap());
|
||||
}
|
||||
headers
|
||||
}
|
||||
}
|
||||
@ -183,21 +183,15 @@ composite_rejection! {
|
||||
}
|
||||
|
||||
fn json_content_type(headers: &HeaderMap) -> bool {
|
||||
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
||||
content_type
|
||||
} else {
|
||||
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let content_type = if let Ok(content_type) = content_type.to_str() {
|
||||
content_type
|
||||
} else {
|
||||
let Ok(content_type) = content_type.to_str() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
|
||||
mime
|
||||
} else {
|
||||
let Ok(mime) = content_type.parse::<mime::Mime>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
//! Additional extractors.
|
||||
|
||||
mod host;
|
||||
pub mod rejection;
|
||||
|
||||
#[cfg(feature = "optional-path")]
|
||||
mod optional_path;
|
||||
|
||||
#[cfg(feature = "cached")]
|
||||
mod cached;
|
||||
|
||||
@ -27,15 +21,6 @@ mod query;
|
||||
#[cfg(feature = "multipart")]
|
||||
pub mod multipart;
|
||||
|
||||
#[cfg(feature = "scheme")]
|
||||
mod scheme;
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "optional-path")]
|
||||
pub use self::optional_path::OptionalPath;
|
||||
|
||||
pub use self::host::Host;
|
||||
|
||||
#[cfg(feature = "cached")]
|
||||
pub use self::cached::Cached;
|
||||
|
||||
@ -52,20 +37,18 @@ pub use self::cookie::PrivateCookieJar;
|
||||
pub use self::cookie::SignedCookieJar;
|
||||
|
||||
#[cfg(feature = "form")]
|
||||
#[allow(deprecated)]
|
||||
pub use self::form::{Form, FormRejection};
|
||||
|
||||
#[cfg(feature = "query")]
|
||||
pub use self::query::OptionalQuery;
|
||||
#[cfg(feature = "query")]
|
||||
#[allow(deprecated)]
|
||||
pub use self::query::{OptionalQueryRejection, Query, QueryRejection};
|
||||
|
||||
#[cfg(feature = "multipart")]
|
||||
pub use self::multipart::Multipart;
|
||||
|
||||
#[cfg(feature = "scheme")]
|
||||
#[doc(no_inline)]
|
||||
pub use self::scheme::{Scheme, SchemeMissing};
|
||||
|
||||
#[cfg(feature = "json-deserializer")]
|
||||
pub use self::json_deserializer::{
|
||||
JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError,
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
use axum::{
|
||||
extract::{rejection::PathRejection, FromRequestParts, Path},
|
||||
RequestPartsExt,
|
||||
};
|
||||
use serde_core::de::DeserializeOwned;
|
||||
|
||||
/// Extractor that extracts path arguments the same way as [`Path`], except if there aren't any.
|
||||
///
|
||||
/// This extractor can be used in place of `Path` when you have two routes that you want to handle
|
||||
/// in mostly the same way, where one has a path parameter and the other one doesn't.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::NonZeroU32;
|
||||
/// use axum::{
|
||||
/// response::IntoResponse,
|
||||
/// routing::get,
|
||||
/// Router,
|
||||
/// };
|
||||
/// use axum_extra::extract::OptionalPath;
|
||||
///
|
||||
/// async fn render_blog(OptionalPath(page): OptionalPath<NonZeroU32>) -> impl IntoResponse {
|
||||
/// // Convert to u32, default to page 1 if not specified
|
||||
/// let page = page.map_or(1, |param| param.get());
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new()
|
||||
/// .route("/blog", get(render_blog))
|
||||
/// .route("/blog/{page}", get(render_blog));
|
||||
/// # let app: Router = app;
|
||||
/// ```
|
||||
#[deprecated = "Use Option<Path<_>> instead"]
|
||||
#[derive(Debug)]
|
||||
pub struct OptionalPath<T>(pub Option<T>);
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> FromRequestParts<S> for OptionalPath<T>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = PathRejection;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
_: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.extract::<Option<Path<T>>>()
|
||||
.await
|
||||
.map(|opt| Self(opt.map(|Path(x)| x)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(deprecated)]
|
||||
mod tests {
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use axum::{routing::get, Router};
|
||||
|
||||
use super::OptionalPath;
|
||||
use crate::test_helpers::TestClient;
|
||||
|
||||
#[crate::test]
|
||||
async fn supports_128_bit_numbers() {
|
||||
async fn handle(OptionalPath(param): OptionalPath<NonZeroU32>) -> String {
|
||||
let num = param.map_or(0, |p| p.get());
|
||||
format!("Success: {num}")
|
||||
}
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(handle))
|
||||
.route("/{num}", get(handle));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/").await;
|
||||
assert_eq!(res.text().await, "Success: 0");
|
||||
|
||||
let res = client.get("/1").await;
|
||||
assert_eq!(res.text().await, "Success: 1");
|
||||
|
||||
let res = client.get("/0").await;
|
||||
assert_eq!(
|
||||
res.text().await,
|
||||
"Invalid URL: invalid value: integer `0`, expected a nonzero u32"
|
||||
);
|
||||
|
||||
let res = client.get("/NaN").await;
|
||||
assert_eq!(
|
||||
res.text().await,
|
||||
"Invalid URL: Cannot parse `NaN` to a `u32`"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use axum_core::__composite_rejection as composite_rejection;
|
||||
use axum_core::__define_rejection as define_rejection;
|
||||
use axum_core::extract::FromRequestParts;
|
||||
@ -8,72 +10,16 @@ use serde_core::de::DeserializeOwned;
|
||||
///
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Differences from `axum::extract::Query`
|
||||
/// # Deprecated
|
||||
///
|
||||
/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
|
||||
/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
|
||||
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
|
||||
/// container.
|
||||
/// This extractor used to use a different deserializer under-the-hood but that
|
||||
/// is no longer the case. Now it only uses an older version of the same
|
||||
/// deserializer, purely for ease of transition to the latest version.
|
||||
/// Before switching to `axum::extract::Form`, it is recommended to read the
|
||||
/// [changelog for `serde_html_form v0.3.0`][changelog].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::{routing::get, Router};
|
||||
/// use axum_extra::extract::Query;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Pagination {
|
||||
/// page: usize,
|
||||
/// per_page: usize,
|
||||
/// }
|
||||
///
|
||||
/// // This will parse query strings like `?page=2&per_page=30` into `Pagination`
|
||||
/// // structs.
|
||||
/// async fn list_things(pagination: Query<Pagination>) {
|
||||
/// let pagination: Pagination = pagination.0;
|
||||
///
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/list_things", get(list_things));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// If the query string cannot be parsed it will reject the request with a `400
|
||||
/// Bad Request` response.
|
||||
///
|
||||
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
|
||||
/// example.
|
||||
///
|
||||
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
|
||||
///
|
||||
/// While `Option<T>` will handle empty parameters (e.g. `param=`), beware when using this with a
|
||||
/// `Vec<T>`. If your list is optional, use `Vec<T>` in combination with `#[serde(default)]`
|
||||
/// instead of `Option<Vec<T>>`. `Option<Vec<T>>` will handle 0, 2, or more arguments, but not one
|
||||
/// argument.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::{routing::get, Router};
|
||||
/// use axum_extra::extract::Query;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Params {
|
||||
/// #[serde(default)]
|
||||
/// items: Vec<usize>,
|
||||
/// }
|
||||
///
|
||||
/// // This will parse 0 occurrences of `items` as an empty `Vec`.
|
||||
/// async fn process_items(Query(params): Query<Params>) {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/process_items", get(process_items));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
|
||||
#[deprecated = "see documentation"]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Query<T>(pub T);
|
||||
@ -91,7 +37,7 @@ where
|
||||
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
|
||||
let value = serde_path_to_error::deserialize(deserializer)
|
||||
.map_err(FailedToDeserializeQueryString::from_err)?;
|
||||
Ok(Query(value))
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,6 +86,7 @@ composite_rejection! {
|
||||
/// Rejection used for [`Query`].
|
||||
///
|
||||
/// Contains one variant for each way the [`Query`] extractor can fail.
|
||||
#[deprecated = "because Query is deprecated"]
|
||||
pub enum QueryRejection {
|
||||
FailedToDeserializeQueryString,
|
||||
}
|
||||
@ -147,7 +94,7 @@ composite_rejection! {
|
||||
|
||||
/// Extractor that deserializes query strings into `None` if no query parameters are present.
|
||||
///
|
||||
/// Otherwise behaviour is identical to [`Query`].
|
||||
/// Otherwise behaviour is identical to [`Query`][axum::extract::Query].
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Example
|
||||
@ -179,11 +126,6 @@ composite_rejection! {
|
||||
///
|
||||
/// If the query string cannot be parsed it will reject the request with a `400
|
||||
/// Bad Request` response.
|
||||
///
|
||||
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
|
||||
/// example.
|
||||
///
|
||||
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct OptionalQuery<T>(pub Option<T>);
|
||||
@ -201,9 +143,9 @@ where
|
||||
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
|
||||
let value = serde_path_to_error::deserialize(deserializer)
|
||||
.map_err(FailedToDeserializeQueryString::from_err)?;
|
||||
Ok(OptionalQuery(Some(value)))
|
||||
Ok(Self(Some(value)))
|
||||
} else {
|
||||
Ok(OptionalQuery(None))
|
||||
Ok(Self(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
//! 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,
|
||||
}
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
//! Extractor that parses the scheme of a request.
|
||||
//! See [`Scheme`] for more details.
|
||||
|
||||
use axum_core::{__define_rejection as define_rejection, extract::FromRequestParts};
|
||||
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);
|
||||
|
||||
define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "No scheme found in request"]
|
||||
/// Rejection type used if the [`Scheme`] extractor is unable to
|
||||
/// resolve a scheme.
|
||||
pub struct SchemeMissing;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -119,7 +119,7 @@ where
|
||||
|
||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let extractor = E::from_request(req, state).await?;
|
||||
Ok(WithRejection(extractor, PhantomData))
|
||||
Ok(Self(extractor, PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ where
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let extractor = E::from_request_parts(parts, state).await?;
|
||||
Ok(WithRejection(extractor, PhantomData))
|
||||
Ok(Self(extractor, PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +188,7 @@ mod tests {
|
||||
|
||||
impl From<()> for TestRejection {
|
||||
fn from(_: ()) -> Self {
|
||||
TestRejection
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,8 @@ use axum::{
|
||||
handler::Handler,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use futures_util::future::{BoxFuture, FutureExt, Map};
|
||||
use futures_core::future::BoxFuture;
|
||||
use futures_util::future::{FutureExt, Map};
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
mod or;
|
||||
|
||||
@ -5,7 +5,8 @@ use axum::{
|
||||
handler::Handler,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use futures_util::future::{BoxFuture, Either as EitherFuture, FutureExt, Map};
|
||||
use futures_core::future::BoxFuture;
|
||||
use futures_util::future::{Either as EitherFuture, FutureExt, Map};
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
/// [`Handler`] that runs one [`Handler`] and if that rejects it'll fallback to another
|
||||
|
||||
@ -4,10 +4,11 @@ use axum_core::{
|
||||
body::Body,
|
||||
extract::{FromRequest, Request},
|
||||
response::{IntoResponse, Response},
|
||||
BoxError,
|
||||
BoxError, RequestExt,
|
||||
};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use futures_util::stream::{BoxStream, Stream, TryStream, TryStreamExt};
|
||||
use futures_core::{stream::BoxStream, Stream, TryStream};
|
||||
use futures_util::stream::TryStreamExt;
|
||||
use pin_project_lite::pin_project;
|
||||
use serde_core::{de::DeserializeOwned, Serialize};
|
||||
use std::{
|
||||
@ -44,7 +45,7 @@ pin_project! {
|
||||
/// ```rust
|
||||
/// use axum::{BoxError, response::{IntoResponse, Response}};
|
||||
/// use axum_extra::json_lines::JsonLines;
|
||||
/// use futures_util::stream::Stream;
|
||||
/// use futures_core::stream::Stream;
|
||||
///
|
||||
/// fn stream_of_values() -> impl Stream<Item = Result<serde_json::Value, BoxError>> {
|
||||
/// # futures_util::stream::empty()
|
||||
@ -108,7 +109,7 @@ where
|
||||
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
// `Stream::lines` isn't a thing so we have to convert it into an `AsyncRead`
|
||||
// so we can call `AsyncRead::lines` and then convert it back to a `Stream`
|
||||
let body = req.into_body();
|
||||
let body = req.into_limited_body();
|
||||
let stream = body.into_data_stream();
|
||||
let stream = stream.map_err(io::Error::other);
|
||||
let read = StreamReader::new(stream);
|
||||
|
||||
@ -18,15 +18,14 @@
|
||||
//! `cookie-key-expansion` | Enables the [`Key::derive_from`](crate::extract::cookie::Key::derive_from) method |
|
||||
//! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response |
|
||||
//! `error-response` | Enables the [`InternalServerError`](crate::response::InternalServerError) response |
|
||||
//! `form` | Enables the [`Form`](crate::extract::Form) extractor |
|
||||
//! `form` (deprecated) | Enables the [`Form`](crate::extract::Form) extractor |
|
||||
//! `handler` | Enables the [handler] utilities |
|
||||
//! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor |
|
||||
//! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response |
|
||||
//! `middleware` | Enables the [middleware] utilities |
|
||||
//! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor |
|
||||
//! `optional-path` | Enables the [`OptionalPath`](crate::extract::OptionalPath) extractor |
|
||||
//! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response |
|
||||
//! `query` | Enables the [`Query`](crate::extract::Query) extractor |
|
||||
//! `query` (deprecated) | Enables the [`Query`](crate::extract::Query) extractor |
|
||||
//! `routing` | Enables the [routing] utilities |
|
||||
//! `tracing` | Log rejections from built-in extractors | <span role="img" aria-label="Default feature">✔</span>
|
||||
//! `typed-routing` | Enables the [`TypedPath`](crate::routing::TypedPath) routing utilities and the `routing` feature. |
|
||||
@ -88,8 +87,5 @@ pub mod __private {
|
||||
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use axum_macros::__private_axum_test as test;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use axum::test_helpers;
|
||||
|
||||
@ -4,7 +4,7 @@ use axum_core::__composite_rejection as composite_rejection;
|
||||
use axum_core::__define_rejection as define_rejection;
|
||||
use axum_core::{
|
||||
extract::{rejection::BytesRejection, FromRequest, Request},
|
||||
response::{IntoResponse, Response},
|
||||
response::{IntoResponse, IntoResponseFailed, Response},
|
||||
RequestExt,
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
@ -109,7 +109,7 @@ where
|
||||
.aggregate();
|
||||
|
||||
match T::decode(&mut buf) {
|
||||
Ok(value) => Ok(Protobuf(value)),
|
||||
Ok(value) => Ok(Self(value)),
|
||||
Err(err) => Err(ProtobufDecodeError::from_err(err).into()),
|
||||
}
|
||||
}
|
||||
@ -131,7 +131,12 @@ where
|
||||
let mut buf = BytesMut::with_capacity(self.0.encoded_len());
|
||||
match &self.0.encode(&mut buf) {
|
||||
Ok(()) => buf.into_response(),
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
IntoResponseFailed,
|
||||
err.to_string(),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,6 +166,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{routing::post, Router};
|
||||
use http::header::CONTENT_TYPE;
|
||||
use http::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn decode_body() {
|
||||
@ -172,7 +179,7 @@ mod tests {
|
||||
|
||||
let app = Router::new().route(
|
||||
"/",
|
||||
post(|input: Protobuf<Input>| async move { input.foo.to_owned() }),
|
||||
post(|Protobuf(input): Protobuf<Input>| async move { input.foo }),
|
||||
);
|
||||
|
||||
let input = Input {
|
||||
@ -182,9 +189,11 @@ mod tests {
|
||||
let client = TestClient::new(app);
|
||||
let res = client.post("/").body(input.encode_to_vec()).await;
|
||||
|
||||
let status = res.status();
|
||||
let body = res.text().await;
|
||||
|
||||
assert_eq!(body, "bar");
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body, input.foo);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -211,6 +220,7 @@ mod tests {
|
||||
let res = client.post("/").body(input.encode_to_vec()).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
assert!(res.text().await.starts_with("Failed to decode the body"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -228,10 +238,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn handler(input: Protobuf<Input>) -> Protobuf<Output> {
|
||||
let output = Output {
|
||||
result: input.foo.to_owned(),
|
||||
};
|
||||
async fn handler(Protobuf(input): Protobuf<Input>) -> Protobuf<Output> {
|
||||
let output = Output { result: input.foo };
|
||||
|
||||
Protobuf(output)
|
||||
}
|
||||
@ -245,8 +253,13 @@ mod tests {
|
||||
let client = TestClient::new(app);
|
||||
let res = client.post("/").body(input.encode_to_vec()).await;
|
||||
|
||||
let content_type_header_value = res
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.expect("missing expected header");
|
||||
|
||||
assert_eq!(
|
||||
res.headers()["content-type"],
|
||||
content_type_header_value,
|
||||
mime::APPLICATION_OCTET_STREAM.as_ref()
|
||||
);
|
||||
|
||||
@ -254,6 +267,6 @@ mod tests {
|
||||
|
||||
let output = Output::decode(body).unwrap();
|
||||
|
||||
assert_eq!(output.result, "bar");
|
||||
assert_eq!(output.result, input.foo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum_core::response::{IntoResponse, Response};
|
||||
use axum_core::response::{IntoResponse, IntoResponseFailed, Response};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use http::{header, HeaderValue, StatusCode};
|
||||
use serde_core::Serialize;
|
||||
@ -57,7 +57,10 @@ impl ErasedJson {
|
||||
pub fn pretty<T: Serialize>(val: T) -> Self {
|
||||
let mut bytes = BytesMut::with_capacity(128);
|
||||
let result = match serde_json::to_writer_pretty((&mut bytes).writer(), &val) {
|
||||
Ok(()) => Ok(bytes.freeze()),
|
||||
Ok(()) => {
|
||||
bytes.put_u8(b'\n');
|
||||
Ok(bytes.freeze())
|
||||
}
|
||||
Err(e) => Err(Arc::new(e)),
|
||||
};
|
||||
Self(result)
|
||||
@ -75,7 +78,12 @@ impl IntoResponse for ErasedJson {
|
||||
bytes,
|
||||
)
|
||||
.into_response(),
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
IntoResponseFailed,
|
||||
err.to_string(),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -127,6 +135,7 @@ impl IntoResponse for ErasedJson {
|
||||
/// ```
|
||||
/// let response = axum_extra::json!(["trailing",]);
|
||||
/// ```
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "erased-json")))]
|
||||
#[macro_export]
|
||||
macro_rules! json {
|
||||
($($t:tt)*) => {
|
||||
|
||||
@ -6,9 +6,9 @@ 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.
|
||||
/// the `IntoResponse` trait itself. Error details are logged using [`tracing::error!`]
|
||||
/// and a generic `500 Internal Server Error` response is returned to the client without
|
||||
/// exposing error details.
|
||||
///
|
||||
/// ```rust
|
||||
/// use axum_extra::response::InternalServerError;
|
||||
|
||||
@ -4,7 +4,7 @@ use axum_core::{
|
||||
BoxError,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_util::TryStream;
|
||||
use futures_core::TryStream;
|
||||
use http::{header, StatusCode};
|
||||
use std::{io, path::Path};
|
||||
use tokio::{
|
||||
@ -191,6 +191,10 @@ where
|
||||
let metadata = file.metadata().await?;
|
||||
let total_size = metadata.len();
|
||||
|
||||
if total_size == 0 {
|
||||
return Ok((StatusCode::RANGE_NOT_SATISFIABLE, "Range Not Satisfiable").into_response());
|
||||
}
|
||||
|
||||
if end == 0 {
|
||||
end = total_size - 1;
|
||||
}
|
||||
@ -284,7 +288,7 @@ where
|
||||
.unwrap_or_else(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("build FileStream responsec error: {e}"),
|
||||
format!("build FileStream response error: {e}"),
|
||||
)
|
||||
.into_response()
|
||||
})
|
||||
@ -596,4 +600,52 @@ mod tests {
|
||||
}
|
||||
Some((start, end))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn response_range_empty_file() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = tempfile::NamedTempFile::new()?;
|
||||
file.as_file().set_len(0)?;
|
||||
let path = file.path().to_owned();
|
||||
|
||||
let app = Router::new().route(
|
||||
"/range_empty",
|
||||
get(move |headers: HeaderMap| {
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let range_header = headers
|
||||
.get(header::RANGE)
|
||||
.and_then(|value| value.to_str().ok());
|
||||
|
||||
let (start, end) = if let Some(range) = range_header {
|
||||
if let Some(range) = parse_range_header(range) {
|
||||
range
|
||||
} else {
|
||||
return (StatusCode::RANGE_NOT_SATISFIABLE, "Invalid Range")
|
||||
.into_response();
|
||||
}
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
FileStream::<ReaderStream<File>>::try_range_response(path, start, end)
|
||||
.await
|
||||
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/range_empty")
|
||||
.header(header::RANGE, "bytes=0-")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ impl MultipartForm {
|
||||
/// let form = MultipartForm::with_parts(parts);
|
||||
/// ```
|
||||
pub fn with_parts(parts: Vec<Part>) -> Self {
|
||||
MultipartForm { parts }
|
||||
Self { parts }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
use axum::{
|
||||
extract::{OriginalUri, Request},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::{any, MethodRouter},
|
||||
routing::{any, on, MethodFilter, MethodRouter},
|
||||
Router,
|
||||
};
|
||||
use http::{uri::PathAndQuery, StatusCode, Uri};
|
||||
@ -68,6 +68,7 @@ pub const fn __private_validate_static_path(path: &'static str) -> &'static str
|
||||
/// ```
|
||||
///
|
||||
/// This macro is available only on rust versions 1.80 and above.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "routing")))]
|
||||
#[rustversion::since(1.80)]
|
||||
#[macro_export]
|
||||
macro_rules! vpath {
|
||||
@ -336,8 +337,9 @@ where
|
||||
Self: Sized,
|
||||
{
|
||||
validate_tsr_path(path);
|
||||
let method_filter = method_router.method_filter();
|
||||
self = self.route(path, method_router);
|
||||
add_tsr_redirect_route(self, path)
|
||||
add_tsr_redirect_route(self, path, method_filter)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@ -350,7 +352,7 @@ where
|
||||
{
|
||||
validate_tsr_path(path);
|
||||
self = self.route_service(path, service);
|
||||
add_tsr_redirect_route(self, path)
|
||||
add_tsr_redirect_route(self, path, None)
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,7 +363,11 @@ fn validate_tsr_path(path: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_tsr_redirect_route<S>(router: Router<S>, path: &str) -> Router<S>
|
||||
fn add_tsr_redirect_route<S>(
|
||||
router: Router<S>,
|
||||
path: &str,
|
||||
method_filter: Option<MethodFilter>,
|
||||
) -> Router<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
@ -373,17 +379,27 @@ where
|
||||
});
|
||||
|
||||
if let Some(new_uri) = new_uri {
|
||||
Redirect::permanent(&new_uri.to_string()).into_response()
|
||||
Redirect::permanent(new_uri.to_string()).into_response()
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path_without_trailing_slash) = path.strip_suffix('/') {
|
||||
router.route(path_without_trailing_slash, any(redirect_handler))
|
||||
let _slot;
|
||||
let redirect_path = if let Some(without_slash) = path.strip_suffix('/') {
|
||||
without_slash
|
||||
} else {
|
||||
router.route(&format!("{path}/"), any(redirect_handler))
|
||||
}
|
||||
// FIXME: Can return `&format!(...)` directly when MSRV is updated
|
||||
_slot = format!("{path}/");
|
||||
&_slot
|
||||
};
|
||||
|
||||
let method_router = match method_filter {
|
||||
Some(f) => on(f, redirect_handler),
|
||||
None => any(redirect_handler),
|
||||
};
|
||||
|
||||
router.route(redirect_path, method_router)
|
||||
}
|
||||
|
||||
/// Map the path of a `Uri`.
|
||||
@ -417,7 +433,10 @@ mod sealed {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{extract::Path, routing::get};
|
||||
use axum::{
|
||||
extract::Path,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tsr() {
|
||||
@ -500,6 +519,13 @@ mod tests {
|
||||
assert_eq!(res.headers()["location"], "/neko/nyan/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tsr_independent_route_registration() {
|
||||
let _: Router = Router::new()
|
||||
.route_with_tsr("/x", get(|| async {}))
|
||||
.route_with_tsr("/x", post(|| async {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "Cannot add a trailing slash redirect route for `/`"]
|
||||
fn tsr_at_root() {
|
||||
|
||||
@ -320,7 +320,7 @@ where
|
||||
/// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
|
||||
/// given type.
|
||||
///
|
||||
/// If you see it in type errors it's most likely because the second argument to your handler doesn't
|
||||
/// If you see it in type errors it's most likely because the first argument to your handler doesn't
|
||||
/// implement [`TypedPath`].
|
||||
///
|
||||
/// You normally shouldn't have to use this trait directly.
|
||||
@ -384,12 +384,7 @@ impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::routing::{RouterExt, TypedPath};
|
||||
use axum::{
|
||||
extract::rejection::PathRejection,
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
};
|
||||
use crate::routing::TypedPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
@ -441,6 +436,12 @@ mod tests {
|
||||
#[cfg(feature = "with-rejection")]
|
||||
#[allow(dead_code)] // just needs to compile
|
||||
fn supports_with_rejection() {
|
||||
use crate::routing::RouterExt;
|
||||
use axum::{
|
||||
extract::rejection::PathRejection,
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
};
|
||||
async fn handler(_: crate::extract::WithRejection<UsersShow, MyRejection>) {}
|
||||
|
||||
struct MyRejection {}
|
||||
|
||||
@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Unreleased
|
||||
|
||||
- **breaking:** `#[from_request(via(Extractor))]` now uses the extractor's
|
||||
rejection type instead of `axum::response::Response` ([#3261])
|
||||
|
||||
[#3261]: https://github.com/tokio-rs/axum/pull/3261
|
||||
|
||||
# 0.5.0
|
||||
|
||||
*No changes since alpha.1*
|
||||
|
||||
@ -11,13 +11,16 @@ readme = "README.md"
|
||||
repository = "https://github.com/tokio-rs/axum"
|
||||
version = "0.5.0" # remember to also bump the version that axum and axum-extra depends on
|
||||
|
||||
[features]
|
||||
default = []
|
||||
__private = ["syn/visit-mut"]
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
__private = ["syn/visit-mut"]
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
@ -40,9 +43,3 @@ trybuild = "1.0.63"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.cargo-public-api-crates]
|
||||
allowed = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
Copyright 2021 axum Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
axum-macros/LICENSE
Symbolic link
1
axum-macros/LICENSE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@ -1 +1 @@
|
||||
nightly-2024-06-22
|
||||
nightly-2025-09-28
|
||||
|
||||
@ -8,16 +8,16 @@ use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, ReturnType, Token, Type};
|
||||
|
||||
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenStream {
|
||||
pub(crate) fn expand(attr: Attrs, item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
|
||||
let Attrs { state_ty } = attr;
|
||||
|
||||
let mut state_ty = state_ty.map(second);
|
||||
|
||||
let check_extractor_count = check_extractor_count(&item_fn, kind);
|
||||
let check_path_extractor = check_path_extractor(&item_fn, kind);
|
||||
let check_output_tuples = check_output_tuples(&item_fn);
|
||||
let check_extractor_count = check_extractor_count(item_fn, kind);
|
||||
let check_path_extractor = check_path_extractor(item_fn, kind);
|
||||
let check_output_tuples = check_output_tuples(item_fn);
|
||||
let check_output_impls_into_response = if check_output_tuples.is_empty() {
|
||||
check_output_impls_into_response(&item_fn)
|
||||
check_output_impls_into_response(item_fn)
|
||||
} else {
|
||||
check_output_tuples
|
||||
};
|
||||
@ -28,7 +28,7 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenS
|
||||
let mut err = None;
|
||||
|
||||
if state_ty.is_none() {
|
||||
let state_types_from_args = state_types_from_args(&item_fn);
|
||||
let state_types_from_args = state_types_from_args(item_fn);
|
||||
|
||||
#[allow(clippy::comparison_chain)]
|
||||
if state_types_from_args.len() == 1 {
|
||||
@ -50,16 +50,16 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenS
|
||||
err.unwrap_or_else(|| {
|
||||
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(()));
|
||||
|
||||
let check_future_send = check_future_send(&item_fn, kind);
|
||||
let check_future_send = check_future_send(item_fn, kind);
|
||||
|
||||
if let Some(check_input_order) = check_input_order(&item_fn, kind) {
|
||||
if let Some(check_input_order) = check_input_order(item_fn, kind) {
|
||||
quote! {
|
||||
#check_input_order
|
||||
#check_future_send
|
||||
}
|
||||
} else {
|
||||
let check_inputs_impls_from_request =
|
||||
check_inputs_impls_from_request(&item_fn, state_ty, kind);
|
||||
check_inputs_impls_from_request(item_fn, &state_ty, kind);
|
||||
|
||||
quote! {
|
||||
#check_inputs_impls_from_request
|
||||
@ -76,7 +76,7 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenS
|
||||
};
|
||||
|
||||
let middleware_takes_next_as_last_arg =
|
||||
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
|
||||
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(item_fn));
|
||||
|
||||
quote! {
|
||||
#item_fn
|
||||
@ -97,17 +97,17 @@ pub(crate) enum FunctionKind {
|
||||
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"),
|
||||
Self::Handler => f.write_str("handler"),
|
||||
Self::Middleware => f.write_str("middleware"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionKind {
|
||||
fn name_uppercase_plural(&self) -> &'static str {
|
||||
fn name_uppercase_plural(self) -> &'static str {
|
||||
match self {
|
||||
FunctionKind::Handler => "Handlers",
|
||||
FunctionKind::Middleware => "Middleware",
|
||||
Self::Handler => "Handlers",
|
||||
Self::Middleware => "Middleware",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -222,7 +222,7 @@ fn is_self_pat_type(typed: &syn::PatType) -> bool {
|
||||
|
||||
fn check_inputs_impls_from_request(
|
||||
item_fn: &ItemFn,
|
||||
state_ty: Type,
|
||||
state_ty: &Type,
|
||||
kind: FunctionKind,
|
||||
) -> TokenStream {
|
||||
let takes_self = item_fn.sig.inputs.first().is_some_and(|arg| match arg {
|
||||
|
||||
@ -19,10 +19,10 @@ pub(crate) enum Trait {
|
||||
}
|
||||
|
||||
impl Trait {
|
||||
fn via_marker_type(&self) -> Option<Type> {
|
||||
fn via_marker_type(self) -> Option<Type> {
|
||||
match self {
|
||||
Trait::FromRequest => Some(parse_quote!(M)),
|
||||
Trait::FromRequestParts => None,
|
||||
Self::FromRequest => Some(parse_quote!(M)),
|
||||
Self::FromRequestParts => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,8 +30,8 @@ impl Trait {
|
||||
impl fmt::Display for Trait {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Trait::FromRequest => f.write_str("FromRequest"),
|
||||
Trait::FromRequestParts => f.write_str("FromRequestParts"),
|
||||
Self::FromRequest => f.write_str("FromRequest"),
|
||||
Self::FromRequestParts => f.write_str("FromRequestParts"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,9 +50,9 @@ impl State {
|
||||
/// ```
|
||||
fn impl_generics(&self) -> impl Iterator<Item = Type> {
|
||||
match self {
|
||||
State::Default(inner) => Some(inner.clone()),
|
||||
State::Custom(_) => None,
|
||||
State::CannotInfer => Some(parse_quote!(S)),
|
||||
Self::Default(inner) => Some(inner.clone()),
|
||||
Self::Custom(_) => None,
|
||||
Self::CannotInfer => Some(parse_quote!(S)),
|
||||
}
|
||||
.into_iter()
|
||||
}
|
||||
@ -63,18 +63,18 @@ impl State {
|
||||
/// ```
|
||||
fn trait_generics(&self) -> impl Iterator<Item = Type> {
|
||||
match self {
|
||||
State::Default(inner) | State::Custom(inner) => iter::once(inner.clone()),
|
||||
State::CannotInfer => iter::once(parse_quote!(S)),
|
||||
Self::Default(inner) | Self::Custom(inner) => iter::once(inner.clone()),
|
||||
Self::CannotInfer => iter::once(parse_quote!(S)),
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self) -> TokenStream {
|
||||
match self {
|
||||
State::Custom(_) => quote! {},
|
||||
State::Default(inner) => quote! {
|
||||
Self::Custom(_) => quote! {},
|
||||
Self::Default(inner) => quote! {
|
||||
#inner: ::std::marker::Send + ::std::marker::Sync,
|
||||
},
|
||||
State::CannotInfer => quote! {
|
||||
Self::CannotInfer => quote! {
|
||||
S: ::std::marker::Send + ::std::marker::Sync,
|
||||
},
|
||||
}
|
||||
@ -84,8 +84,8 @@ impl State {
|
||||
impl ToTokens for State {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
State::Custom(inner) | State::Default(inner) => inner.to_tokens(tokens),
|
||||
State::CannotInfer => quote! { S }.to_tokens(tokens),
|
||||
Self::Custom(inner) | Self::Default(inner) => inner.to_tokens(tokens),
|
||||
Self::CannotInfer => quote! { S }.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,43 +111,42 @@ pub(crate) fn expand(item: syn::Item, tr: Trait) -> syn::Result<TokenStream> {
|
||||
state,
|
||||
} = parse_attrs("from_request", &attrs)?;
|
||||
|
||||
let state = match state {
|
||||
Some((_, state)) => State::Custom(state),
|
||||
None => {
|
||||
let mut inferred_state_types: HashSet<_> =
|
||||
infer_state_type_from_field_types(&fields)
|
||||
.chain(infer_state_type_from_field_attributes(&fields))
|
||||
.collect();
|
||||
let state = if let Some((_, state)) = state {
|
||||
State::Custom(state)
|
||||
} else {
|
||||
let mut inferred_state_types: HashSet<_> =
|
||||
infer_state_type_from_field_types(&fields)
|
||||
.chain(infer_state_type_from_field_attributes(&fields))
|
||||
.collect();
|
||||
|
||||
if let Some((_, via)) = &via {
|
||||
inferred_state_types.extend(state_from_via(&ident, via));
|
||||
}
|
||||
if let Some((_, via)) = &via {
|
||||
inferred_state_types.extend(state_from_via(&ident, via));
|
||||
}
|
||||
|
||||
match inferred_state_types.len() {
|
||||
0 => State::Default(syn::parse_quote!(S)),
|
||||
1 => State::Custom(inferred_state_types.iter().next().unwrap().to_owned()),
|
||||
_ => State::CannotInfer,
|
||||
}
|
||||
match inferred_state_types.len() {
|
||||
0 => State::Default(syn::parse_quote!(S)),
|
||||
1 => State::Custom(inferred_state_types.iter().next().unwrap().to_owned()),
|
||||
_ => State::CannotInfer,
|
||||
}
|
||||
};
|
||||
|
||||
let trait_impl = match (via.map(second), rejection.map(second)) {
|
||||
(Some(via), rejection) => impl_struct_by_extracting_all_at_once(
|
||||
ident,
|
||||
&ident,
|
||||
fields,
|
||||
via,
|
||||
rejection,
|
||||
generic_ident,
|
||||
&via,
|
||||
rejection.as_ref(),
|
||||
generic_ident.as_ref(),
|
||||
&state,
|
||||
tr,
|
||||
)?,
|
||||
(None, rejection) => {
|
||||
error_on_generic_ident(generic_ident, tr)?;
|
||||
impl_struct_by_extracting_each_field(ident, fields, rejection, &state, tr)?
|
||||
impl_struct_by_extracting_each_field(&ident, &fields, rejection, &state, tr)?
|
||||
}
|
||||
};
|
||||
|
||||
if let State::CannotInfer = state {
|
||||
if matches!(state, State::CannotInfer) {
|
||||
let attr_name = match tr {
|
||||
Trait::FromRequest => "from_request",
|
||||
Trait::FromRequestParts => "from_request_parts",
|
||||
@ -207,11 +206,11 @@ pub(crate) fn expand(item: syn::Item, tr: Trait) -> syn::Result<TokenStream> {
|
||||
|
||||
match (via.map(second), rejection) {
|
||||
(Some(via), rejection) => impl_enum_by_extracting_all_at_once(
|
||||
ident,
|
||||
&ident,
|
||||
variants,
|
||||
via,
|
||||
rejection.map(second),
|
||||
state,
|
||||
&via,
|
||||
rejection.map(second).as_ref(),
|
||||
&state,
|
||||
tr,
|
||||
),
|
||||
(None, Some((rejection_kw, _))) => Err(syn::Error::new_spanned(
|
||||
@ -329,29 +328,28 @@ fn error_on_generic_ident(generic_ident: Option<Ident>, tr: Trait) -> syn::Resul
|
||||
}
|
||||
|
||||
fn impl_struct_by_extracting_each_field(
|
||||
ident: syn::Ident,
|
||||
fields: syn::Fields,
|
||||
ident: &syn::Ident,
|
||||
fields: &syn::Fields,
|
||||
rejection: Option<syn::Path>,
|
||||
state: &State,
|
||||
tr: Trait,
|
||||
) -> syn::Result<TokenStream> {
|
||||
let trait_fn_body = match state {
|
||||
State::CannotInfer => quote! {
|
||||
let trait_fn_body = if matches!(state, State::CannotInfer) {
|
||||
quote! {
|
||||
::std::unimplemented!()
|
||||
},
|
||||
_ => {
|
||||
let extract_fields = extract_fields(&fields, &rejection, tr)?;
|
||||
quote! {
|
||||
::std::result::Result::Ok(Self {
|
||||
#(#extract_fields)*
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let extract_fields = extract_fields(fields, rejection.as_ref(), tr)?;
|
||||
quote! {
|
||||
::std::result::Result::Ok(Self {
|
||||
#(#extract_fields)*
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let rejection_ident = if let Some(rejection) = rejection {
|
||||
quote!(#rejection)
|
||||
} else if has_no_fields(&fields) {
|
||||
} else if has_no_fields(fields) {
|
||||
quote!(::std::convert::Infallible)
|
||||
} else {
|
||||
quote!(::axum::response::Response)
|
||||
@ -413,23 +411,22 @@ fn has_no_fields(fields: &syn::Fields) -> bool {
|
||||
|
||||
fn extract_fields(
|
||||
fields: &syn::Fields,
|
||||
rejection: &Option<syn::Path>,
|
||||
rejection: Option<&syn::Path>,
|
||||
tr: Trait,
|
||||
) -> syn::Result<Vec<TokenStream>> {
|
||||
fn member(field: &syn::Field, index: usize) -> TokenStream {
|
||||
match &field.ident {
|
||||
Some(ident) => quote! { #ident },
|
||||
_ => {
|
||||
let member = syn::Member::Unnamed(syn::Index {
|
||||
index: index as u32,
|
||||
span: field.span(),
|
||||
});
|
||||
quote! { #member }
|
||||
}
|
||||
if let Some(ident) = &field.ident {
|
||||
quote! { #ident }
|
||||
} else {
|
||||
let member = syn::Member::Unnamed(syn::Index {
|
||||
index: index as u32,
|
||||
span: field.span(),
|
||||
});
|
||||
quote! { #member }
|
||||
}
|
||||
}
|
||||
|
||||
fn into_inner(via: &Option<(attr::kw::via, syn::Path)>, ty_span: Span) -> TokenStream {
|
||||
fn into_inner(via: Option<&(attr::kw::via, syn::Path)>, ty_span: Span) -> TokenStream {
|
||||
if let Some((_, path)) = via {
|
||||
let span = path.span();
|
||||
quote_spanned! {span=>
|
||||
@ -443,7 +440,7 @@ fn extract_fields(
|
||||
}
|
||||
|
||||
fn into_outer(
|
||||
via: &Option<(attr::kw::via, syn::Path)>,
|
||||
via: Option<&(attr::kw::via, syn::Path)>,
|
||||
ty_span: Span,
|
||||
field_ty: &Type,
|
||||
) -> TokenStream {
|
||||
@ -475,10 +472,10 @@ fn extract_fields(
|
||||
|
||||
let member = member(field, index);
|
||||
let ty_span = field.ty.span();
|
||||
let into_inner = into_inner(&via, ty_span);
|
||||
let into_inner = into_inner(via.as_ref(), ty_span);
|
||||
|
||||
if peel_option(&field.ty).is_some() {
|
||||
let field_ty = into_outer(&via, ty_span, peel_option(&field.ty).unwrap());
|
||||
let field_ty = into_outer(via.as_ref(), ty_span, peel_option(&field.ty).unwrap());
|
||||
let tokens = match tr {
|
||||
Trait::FromRequest => {
|
||||
quote_spanned! {ty_span=>
|
||||
@ -513,7 +510,7 @@ fn extract_fields(
|
||||
};
|
||||
Ok(tokens)
|
||||
} else if peel_result_ok(&field.ty).is_some() {
|
||||
let field_ty = into_outer(&via,ty_span, peel_result_ok(&field.ty).unwrap());
|
||||
let field_ty = into_outer(via.as_ref(), ty_span, peel_result_ok(&field.ty).unwrap());
|
||||
let tokens = match tr {
|
||||
Trait::FromRequest => {
|
||||
quote_spanned! {ty_span=>
|
||||
@ -546,7 +543,7 @@ fn extract_fields(
|
||||
};
|
||||
Ok(tokens)
|
||||
} else {
|
||||
let field_ty = into_outer(&via,ty_span,&field.ty);
|
||||
let field_ty = into_outer(via.as_ref(), ty_span, &field.ty);
|
||||
let map_err = if let Some(rejection) = rejection {
|
||||
quote! { <#rejection as ::std::convert::From<_>>::from }
|
||||
} else {
|
||||
@ -596,10 +593,10 @@ fn extract_fields(
|
||||
|
||||
let member = member(field, fields.len() - 1);
|
||||
let ty_span = field.ty.span();
|
||||
let into_inner = into_inner(&via, ty_span);
|
||||
let into_inner = into_inner(via.as_ref(), ty_span);
|
||||
|
||||
let item = if peel_option(&field.ty).is_some() {
|
||||
let field_ty = into_outer(&via, ty_span, peel_option(&field.ty).unwrap());
|
||||
let field_ty = into_outer(via.as_ref(), ty_span, peel_option(&field.ty).unwrap());
|
||||
quote_spanned! {ty_span=>
|
||||
#member: {
|
||||
<#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
|
||||
@ -609,7 +606,7 @@ fn extract_fields(
|
||||
},
|
||||
}
|
||||
} else if peel_result_ok(&field.ty).is_some() {
|
||||
let field_ty = into_outer(&via, ty_span, peel_result_ok(&field.ty).unwrap());
|
||||
let field_ty = into_outer(via.as_ref(), ty_span, peel_result_ok(&field.ty).unwrap());
|
||||
quote_spanned! {ty_span=>
|
||||
#member: {
|
||||
<#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
|
||||
@ -618,7 +615,7 @@ fn extract_fields(
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let field_ty = into_outer(&via, ty_span, &field.ty);
|
||||
let field_ty = into_outer(via.as_ref(), ty_span, &field.ty);
|
||||
let map_err = if let Some(rejection) = rejection {
|
||||
quote! { <#rejection as ::std::convert::From<_>>::from }
|
||||
} else {
|
||||
@ -642,9 +639,7 @@ fn extract_fields(
|
||||
}
|
||||
|
||||
fn peel_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
let type_path = if let syn::Type::Path(type_path) = ty {
|
||||
type_path
|
||||
} else {
|
||||
let syn::Type::Path(type_path) = ty else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@ -673,9 +668,7 @@ fn peel_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
}
|
||||
|
||||
fn peel_result_ok(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
let type_path = if let syn::Type::Path(type_path) = ty {
|
||||
type_path
|
||||
} else {
|
||||
let syn::Type::Path(type_path) = ty else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@ -704,11 +697,11 @@ fn peel_result_ok(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
}
|
||||
|
||||
fn impl_struct_by_extracting_all_at_once(
|
||||
ident: syn::Ident,
|
||||
ident: &syn::Ident,
|
||||
fields: syn::Fields,
|
||||
via_path: syn::Path,
|
||||
rejection: Option<syn::Path>,
|
||||
generic_ident: Option<Ident>,
|
||||
via_path: &syn::Path,
|
||||
rejection: Option<&syn::Path>,
|
||||
generic_ident: Option<&Ident>,
|
||||
state: &State,
|
||||
tr: Trait,
|
||||
) -> syn::Result<TokenStream> {
|
||||
@ -732,18 +725,6 @@ fn impl_struct_by_extracting_all_at_once(
|
||||
|
||||
let path_span = via_path.span();
|
||||
|
||||
let (associated_rejection_type, map_err) = if let Some(rejection) = &rejection {
|
||||
let rejection = quote! { #rejection };
|
||||
let map_err = quote! { ::std::convert::From::from };
|
||||
(rejection, map_err)
|
||||
} else {
|
||||
let rejection = quote! {
|
||||
::axum::response::Response
|
||||
};
|
||||
let map_err = quote! { ::axum::response::IntoResponse::into_response };
|
||||
(rejection, map_err)
|
||||
};
|
||||
|
||||
// for something like
|
||||
//
|
||||
// ```
|
||||
@ -757,7 +738,7 @@ fn impl_struct_by_extracting_all_at_once(
|
||||
// - `State`, not other extractors
|
||||
//
|
||||
// honestly not sure why but the tests all pass
|
||||
let via_marker_type = if path_ident_is_state(&via_path) {
|
||||
let via_marker_type = if path_ident_is_state(via_path) {
|
||||
tr.via_marker_type()
|
||||
} else {
|
||||
None
|
||||
@ -812,6 +793,19 @@ fn impl_struct_by_extracting_all_at_once(
|
||||
quote! { Self }
|
||||
};
|
||||
|
||||
let associated_rejection_type = if let Some(rejection) = &rejection {
|
||||
quote! { #rejection }
|
||||
} else {
|
||||
match tr {
|
||||
Trait::FromRequest => quote! {
|
||||
<#via_path<#via_type_generics> as ::axum::extract::FromRequest<#trait_generics>>::Rejection
|
||||
},
|
||||
Trait::FromRequestParts => quote! {
|
||||
<#via_path<#via_type_generics> as ::axum::extract::FromRequestParts<#trait_generics>>::Rejection
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let value_to_self = if generic_ident.is_some() {
|
||||
quote! {
|
||||
#ident(value)
|
||||
@ -841,7 +835,7 @@ fn impl_struct_by_extracting_all_at_once(
|
||||
<#via_path<#via_type_generics> as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
|
||||
.await
|
||||
.map(|#via_path(value)| #value_to_self)
|
||||
.map_err(#map_err)
|
||||
.map_err(::std::convert::From::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -864,7 +858,7 @@ fn impl_struct_by_extracting_all_at_once(
|
||||
<#via_path<#via_type_generics> as ::axum::extract::FromRequestParts<_>>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map(|#via_path(value)| #value_to_self)
|
||||
.map_err(#map_err)
|
||||
.map_err(::std::convert::From::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -875,11 +869,11 @@ fn impl_struct_by_extracting_all_at_once(
|
||||
}
|
||||
|
||||
fn impl_enum_by_extracting_all_at_once(
|
||||
ident: syn::Ident,
|
||||
ident: &syn::Ident,
|
||||
variants: Punctuated<syn::Variant, Token![,]>,
|
||||
path: syn::Path,
|
||||
rejection: Option<syn::Path>,
|
||||
state: State,
|
||||
path: &syn::Path,
|
||||
rejection: Option<&syn::Path>,
|
||||
state: &State,
|
||||
tr: Trait,
|
||||
) -> syn::Result<TokenStream> {
|
||||
for variant in variants {
|
||||
|
||||
@ -438,7 +438,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
||||
/// let app = Router::new().route("/", get(handler));
|
||||
///
|
||||
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
/// axum::serve(listener, app).await.unwrap();
|
||||
/// axum::serve(listener, app).await;
|
||||
/// }
|
||||
///
|
||||
/// fn handler() -> &'static str {
|
||||
@ -458,7 +458,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
||||
/// # let app = Router::new().route("/", get(handler));
|
||||
/// #
|
||||
/// # let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
/// # axum::serve(listener, app).await.unwrap();
|
||||
/// # axum::serve(listener, app).await;
|
||||
/// # }
|
||||
/// #
|
||||
/// #[debug_handler]
|
||||
@ -485,7 +485,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
||||
/// let app = Router::new().route("/", get(handler));
|
||||
///
|
||||
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
/// axum::serve(listener, app).await.unwrap();
|
||||
/// axum::serve(listener, app).await;
|
||||
/// }
|
||||
///
|
||||
/// #[debug_handler]
|
||||
@ -579,7 +579,7 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
return expand_attr_with(_attr, input, |attrs, item_fn| {
|
||||
debug_handler::expand(attrs, item_fn, FunctionKind::Handler)
|
||||
debug_handler::expand(attrs, &item_fn, FunctionKind::Handler)
|
||||
});
|
||||
}
|
||||
|
||||
@ -607,7 +607,7 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
/// .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();
|
||||
/// axum::serve(listener, app).await;
|
||||
/// }
|
||||
///
|
||||
/// // if this wasn't a valid middleware function #[debug_middleware] would
|
||||
@ -635,7 +635,7 @@ pub fn debug_middleware(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
return expand_attr_with(_attr, input, |attrs, item_fn| {
|
||||
debug_handler::expand(attrs, item_fn, FunctionKind::Middleware)
|
||||
debug_handler::expand(attrs, &item_fn, FunctionKind::Middleware)
|
||||
});
|
||||
}
|
||||
|
||||
@ -662,7 +662,7 @@ pub fn __private_axum_test(_attr: TokenStream, input: TokenStream) -> TokenStrea
|
||||
/// [`axum_extra::routing::TypedPath`]: https://docs.rs/axum-extra/latest/axum_extra/routing/trait.TypedPath.html
|
||||
#[proc_macro_derive(TypedPath, attributes(typed_path))]
|
||||
pub fn derive_typed_path(input: TokenStream) -> TokenStream {
|
||||
expand_with(input, typed_path::expand)
|
||||
expand_with(input, |item_struct| typed_path::expand(&item_struct))
|
||||
}
|
||||
|
||||
/// Derive an implementation of [`FromRef`] for each field in a struct.
|
||||
|
||||
@ -4,14 +4,14 @@ use syn::{parse::Parse, ItemStruct, LitStr, Token};
|
||||
|
||||
use crate::attr_parsing::{combine_attribute, parse_parenthesized_attribute, second, Combine};
|
||||
|
||||
pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
|
||||
pub(crate) fn expand(item_struct: &ItemStruct) -> syn::Result<TokenStream> {
|
||||
let ItemStruct {
|
||||
attrs,
|
||||
ident,
|
||||
generics,
|
||||
fields,
|
||||
..
|
||||
} = &item_struct;
|
||||
} = item_struct;
|
||||
|
||||
if !generics.params.is_empty() || generics.where_clause.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
@ -34,13 +34,18 @@ pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
|
||||
match fields {
|
||||
syn::Fields::Named(_) => {
|
||||
let segments = parse_path(&path)?;
|
||||
Ok(expand_named_fields(ident, path, &segments, rejection))
|
||||
Ok(expand_named_fields(
|
||||
ident,
|
||||
&path,
|
||||
&segments,
|
||||
rejection.as_ref(),
|
||||
))
|
||||
}
|
||||
syn::Fields::Unnamed(fields) => {
|
||||
let segments = parse_path(&path)?;
|
||||
expand_unnamed_fields(fields, ident, path, &segments, rejection)
|
||||
expand_unnamed_fields(fields, ident, &path, &segments, rejection.as_ref())
|
||||
}
|
||||
syn::Fields::Unit => expand_unit_fields(ident, path, rejection),
|
||||
syn::Fields::Unit => expand_unit_fields(ident, &path, rejection.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,9 +100,9 @@ impl Combine for Attrs {
|
||||
|
||||
fn expand_named_fields(
|
||||
ident: &syn::Ident,
|
||||
path: LitStr,
|
||||
path: &LitStr,
|
||||
segments: &[Segment],
|
||||
rejection: Option<syn::Path>,
|
||||
rejection: Option<&syn::Path>,
|
||||
) -> TokenStream {
|
||||
let format_str = format_str_from_path(segments);
|
||||
let captures = captures_from_path(segments);
|
||||
@ -129,8 +134,8 @@ fn expand_named_fields(
|
||||
}
|
||||
};
|
||||
|
||||
let rejection_assoc_type = rejection_assoc_type(&rejection);
|
||||
let map_err_rejection = map_err_rejection(&rejection);
|
||||
let rejection_assoc_type = rejection_assoc_type(rejection);
|
||||
let map_err_rejection = map_err_rejection(rejection);
|
||||
|
||||
let from_request_impl = quote! {
|
||||
#[automatically_derived]
|
||||
@ -162,9 +167,9 @@ fn expand_named_fields(
|
||||
fn expand_unnamed_fields(
|
||||
fields: &syn::FieldsUnnamed,
|
||||
ident: &syn::Ident,
|
||||
path: LitStr,
|
||||
path: &LitStr,
|
||||
segments: &[Segment],
|
||||
rejection: Option<syn::Path>,
|
||||
rejection: Option<&syn::Path>,
|
||||
) -> syn::Result<TokenStream> {
|
||||
let num_captures = segments
|
||||
.iter()
|
||||
@ -233,8 +238,8 @@ fn expand_unnamed_fields(
|
||||
}
|
||||
};
|
||||
|
||||
let rejection_assoc_type = rejection_assoc_type(&rejection);
|
||||
let map_err_rejection = map_err_rejection(&rejection);
|
||||
let rejection_assoc_type = rejection_assoc_type(rejection);
|
||||
let map_err_rejection = map_err_rejection(rejection);
|
||||
|
||||
let from_request_impl = quote! {
|
||||
#[automatically_derived]
|
||||
@ -273,10 +278,10 @@ fn simple_pluralize(count: usize, word: &str) -> String {
|
||||
|
||||
fn expand_unit_fields(
|
||||
ident: &syn::Ident,
|
||||
path: LitStr,
|
||||
rejection: Option<syn::Path>,
|
||||
path: &LitStr,
|
||||
rejection: Option<&syn::Path>,
|
||||
) -> syn::Result<TokenStream> {
|
||||
for segment in parse_path(&path)? {
|
||||
for segment in parse_path(path)? {
|
||||
match segment {
|
||||
Segment::Capture(_, span) => {
|
||||
return Err(syn::Error::new(
|
||||
@ -409,14 +414,14 @@ fn path_rejection() -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn rejection_assoc_type(rejection: &Option<syn::Path>) -> TokenStream {
|
||||
fn rejection_assoc_type(rejection: Option<&syn::Path>) -> TokenStream {
|
||||
match rejection {
|
||||
Some(rejection) => quote! { #rejection },
|
||||
None => path_rejection(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_err_rejection(rejection: &Option<syn::Path>) -> TokenStream {
|
||||
fn map_err_rejection(rejection: Option<&syn::Path>) -> TokenStream {
|
||||
rejection
|
||||
.as_ref()
|
||||
.map(|rejection| {
|
||||
|
||||
@ -40,8 +40,8 @@ impl<I> WithPosition<I>
|
||||
where
|
||||
I: Iterator,
|
||||
{
|
||||
pub(crate) fn new(iter: impl IntoIterator<IntoIter = I>) -> WithPosition<I> {
|
||||
WithPosition {
|
||||
pub(crate) fn new(iter: impl IntoIterator<IntoIter = I>) -> Self {
|
||||
Self {
|
||||
handled_first: false,
|
||||
peekable: iter.into_iter().fuse().peekable(),
|
||||
}
|
||||
@ -72,7 +72,7 @@ pub(crate) enum Position<T> {
|
||||
impl<T> Position<T> {
|
||||
pub(crate) fn into_inner(self) -> T {
|
||||
match self {
|
||||
Position::First(x) | Position::Middle(x) | Position::Last(x) | Position::Only(x) => x,
|
||||
Self::First(x) | Self::Middle(x) | Self::Last(x) | Self::Only(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,19 +2,19 @@ error[E0277]: the trait bound `bool: FromRequest<(), axum_core::extract::private
|
||||
--> tests/debug_handler/fail/argument_not_extractor.rs:4:24
|
||||
|
|
||||
4 | async fn handler(_foo: bool) {}
|
||||
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`, which is required by `bool: FromRequest<(), _>`
|
||||
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`
|
||||
|
|
||||
= note: Function argument is not a valid axum extractor.
|
||||
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details
|
||||
= help: the following other types implement trait `FromRequestParts<S>`:
|
||||
`()` implements `FromRequestParts<S>`
|
||||
`(T1, T2)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
|
||||
`ConnectInfo<T>` implements `FromRequestParts<S>`
|
||||
`Extension<T>` implements `FromRequestParts<S>`
|
||||
`HeaderMap` implements `FromRequestParts<S>`
|
||||
`MatchedPath` implements `FromRequestParts<S>`
|
||||
`Method` implements `FromRequestParts<S>`
|
||||
`OriginalUri` implements `FromRequestParts<S>`
|
||||
`Query<T>` implements `FromRequestParts<S>`
|
||||
`RawPathParams` implements `FromRequestParts<S>`
|
||||
and $N others
|
||||
= note: required for `bool` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
|
||||
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
|
||||
|
||||
@ -2,18 +2,17 @@ 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<(), _>`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonCloneType`
|
||||
|
|
||||
= 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
|
||||
Body
|
||||
Form<T>
|
||||
Json<T>
|
||||
RawForm
|
||||
Result<T, <T as FromRequest<S>>::Rejection>
|
||||
String
|
||||
axum::body::Bytes
|
||||
axum::http::Request<Body>
|
||||
= 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`
|
||||
|
||||
@ -2,8 +2,13 @@ error[E0277]: the trait bound `Struct: serde::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_core::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
|
||||
| ^^^^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `Struct`
|
||||
--> tests/debug_handler/fail/json_not_deserialize.rs:4:1
|
||||
|
|
||||
4 | struct Struct {}
|
||||
| ^^^^^^^^^^^^^
|
||||
= 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_core::de::Deserialize<'de>`:
|
||||
@ -28,8 +33,13 @@ error[E0277]: the trait bound `Struct: serde::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_core::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
|
||||
| ^^^^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `Struct`
|
||||
--> tests/debug_handler/fail/json_not_deserialize.rs:4:1
|
||||
|
|
||||
4 | struct Struct {}
|
||||
| ^^^^^^^^^^^^^
|
||||
= 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_core::de::Deserialize<'de>`:
|
||||
|
||||
@ -4,7 +4,7 @@ error: future cannot be sent between threads safely
|
||||
3 | #[debug_handler]
|
||||
| ^^^^^^^^^^^^^^^^ future returned by `handler` is not `Send`
|
||||
|
|
||||
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`, which is required by `impl Future<Output = ()>: Send`
|
||||
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`
|
||||
note: future is not `Send` as this value is used across an await
|
||||
--> tests/debug_handler/fail/not_send.rs:6:14
|
||||
|
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
error: Cannot return tuples with more than 17 elements
|
||||
--> tests/debug_handler/fail/output_tuple_too_many.rs:4:20
|
||||
|
|
||||
4 | async fn handler() -> (
|
||||
4 | async fn handler() -> (
|
||||
| ____________________^
|
||||
5 | | axum::http::StatusCode,
|
||||
6 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
7 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
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 | | ) {
|
||||
|
||||
@ -2,8 +2,13 @@ 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`
|
||||
| ^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `IntoResponse` is not implemented for `NotIntoResponse`
|
||||
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:3:1
|
||||
|
|
||||
3 | struct NotIntoResponse;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
= help: the following other types implement trait `IntoResponse`:
|
||||
&'static [u8; N]
|
||||
&'static [u8]
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
error: Handlers cannot take more than 16 arguments. Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors
|
||||
--> tests/debug_handler/fail/too_many_extractors.rs:6:5
|
||||
|
|
||||
6 | / _e1: Uri,
|
||||
7 | | _e2: Uri,
|
||||
8 | | _e3: Uri,
|
||||
9 | | _e4: Uri,
|
||||
6 | / _e1: Uri,
|
||||
7 | | _e2: Uri,
|
||||
8 | | _e3: Uri,
|
||||
9 | | _e4: Uri,
|
||||
... |
|
||||
21 | | _e16: Uri,
|
||||
22 | | _e17: Uri,
|
||||
|
||||
@ -8,8 +8,13 @@ error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not sat
|
||||
--> tests/debug_handler/fail/wrong_return_tuple.rs:21:5
|
||||
|
|
||||
21 | CustomIntoResponse,
|
||||
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
||||
| ^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
||||
--> tests/debug_handler/fail/wrong_return_tuple.rs:12:1
|
||||
|
|
||||
12 | struct CustomIntoResponse {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
= help: the following other types implement trait `IntoResponseParts`:
|
||||
()
|
||||
(T1, T2)
|
||||
@ -23,15 +28,20 @@ error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not sat
|
||||
= help: see issue #48214
|
||||
help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
||||
|
|
||||
3 + #![feature(trivial_bounds)]
|
||||
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`
|
||||
| ^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
||||
--> tests/debug_handler/fail/wrong_return_tuple.rs:12:1
|
||||
|
|
||||
12 | struct CustomIntoResponse {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
= help: the following other types implement trait `IntoResponseParts`:
|
||||
()
|
||||
(T1, T2)
|
||||
|
||||
@ -13,9 +13,6 @@ error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future<Output = ()> {fo
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= note: Consider using `#[axum::debug_handler]` to improve the error message
|
||||
= help: the following other types implement trait `Handler<T, S>`:
|
||||
`Layered<L, H, T, S>` implements `Handler<T, S>`
|
||||
`MethodRouter<S>` implements `Handler<(), S>`
|
||||
note: required by a bound in `axum::routing::get`
|
||||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
|
||||
@ -13,9 +13,6 @@ error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future<Output = ()> {fo
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= note: Consider using `#[axum::debug_handler]` to improve the error message
|
||||
= help: the following other types implement trait `Handler<T, S>`:
|
||||
`Layered<L, H, T, S>` implements `Handler<T, S>`
|
||||
`MethodRouter<S>` implements `Handler<(), S>`
|
||||
note: required by a bound in `axum::routing::get`
|
||||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
|
||||
@ -13,9 +13,6 @@ error[E0277]: the trait bound `fn(MyExtractor) -> impl Future<Output = ()> {hand
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= note: Consider using `#[axum::debug_handler]` to improve the error message
|
||||
= help: the following other types implement trait `Handler<T, S>`:
|
||||
`Layered<L, H, T, S>` implements `Handler<T, S>`
|
||||
`MethodRouter<S>` implements `Handler<(), S>`
|
||||
note: required by a bound in `axum::routing::get`
|
||||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
@ -35,9 +32,6 @@ error[E0277]: the trait bound `fn(Result<MyExtractor, MyRejection>) -> impl Futu
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= note: Consider using `#[axum::debug_handler]` to improve the error message
|
||||
= help: the following other types implement trait `Handler<T, S>`:
|
||||
`Layered<L, H, T, S>` implements `Handler<T, S>`
|
||||
`MethodRouter<S>` implements `Handler<(), S>`
|
||||
note: required by a bound in `MethodRouter::<S>::post`
|
||||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
|
||||
@ -7,12 +7,18 @@ error[E0277]: the trait bound `String: FromRequestParts<_>` is not satisfied
|
||||
= note: Function argument is not a valid axum extractor.
|
||||
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details
|
||||
= help: the following other types implement trait `FromRequestParts<S>`:
|
||||
`()` implements `FromRequestParts<S>`
|
||||
`(T1, T2)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
|
||||
`(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
|
||||
`ConnectInfo<T>` implements `FromRequestParts<S>`
|
||||
`Extension<T>` implements `FromRequestParts<S>`
|
||||
`Extractor` implements `FromRequestParts<S>`
|
||||
`HeaderMap` implements `FromRequestParts<S>`
|
||||
`MatchedPath` implements `FromRequestParts<S>`
|
||||
`Method` implements `FromRequestParts<S>`
|
||||
`OriginalUri` implements `FromRequestParts<S>`
|
||||
`Query<T>` implements `FromRequestParts<S>`
|
||||
and $N others
|
||||
|
||||
error[E0282]: type annotations needed
|
||||
--> tests/from_request/fail/parts_extracting_body.rs:5:11
|
||||
|
|
||||
5 | body: String,
|
||||
| ^^^^^^ cannot infer type
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use axum::{
|
||||
extract::{FromRequest, Json},
|
||||
response::Response,
|
||||
use axum::extract::{
|
||||
rejection::JsonRejection,
|
||||
FromRequest,
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
@ -14,7 +15,7 @@ struct Extractor {
|
||||
|
||||
fn assert_from_request()
|
||||
where
|
||||
Extractor: FromRequest<(), Rejection = Response>,
|
||||
Extractor: FromRequest<(), Rejection = JsonRejection>,
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use axum::{
|
||||
extract::{Extension, FromRequestParts},
|
||||
response::Response,
|
||||
use axum::extract::{
|
||||
rejection::ExtensionRejection,
|
||||
Extension,
|
||||
FromRequestParts,
|
||||
};
|
||||
|
||||
#[derive(Clone, FromRequestParts)]
|
||||
@ -13,7 +14,7 @@ struct Extractor {
|
||||
|
||||
fn assert_from_request()
|
||||
where
|
||||
Extractor: FromRequestParts<(), Rejection = Response>,
|
||||
Extractor: FromRequestParts<(), Rejection = ExtensionRejection>,
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -10,5 +10,9 @@ help: include the missing field in the pattern
|
||||
| ++++++
|
||||
help: if you don't care about this missing field, you can explicitly ignore it
|
||||
|
|
||||
5 | #[typed_path("/users" { id: _ })]
|
||||
| +++++++++
|
||||
help: or always ignore missing fields here
|
||||
|
|
||||
5 | #[typed_path("/users" { .. })]
|
||||
| ++++++
|
||||
|
||||
@ -1,9 +1,39 @@
|
||||
error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satisfied
|
||||
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
||||
|
|
||||
3 | #[derive(TypedPath)]
|
||||
| ^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`
|
||||
--> tests/typed_path/fail/not_deserialize.rs:5:1
|
||||
|
|
||||
5 | struct MyPath {
|
||||
| ^^^^^^^^^^^^^
|
||||
= help: the following other types implement trait `serde_core::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 `MyPath` to implement `serde_core::de::DeserializeOwned`
|
||||
= note: required for `axum::extract::Path<MyPath>` to implement `FromRequestParts<S>`
|
||||
= note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0277]: the trait bound `MyPath: serde::Deserialize<'de>` is not satisfied
|
||||
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
||||
|
|
||||
3 | #[derive(TypedPath)]
|
||||
| ^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`, which is required by `axum::extract::Path<MyPath>: FromRequestParts<S>`
|
||||
| ^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`
|
||||
--> tests/typed_path/fail/not_deserialize.rs:5:1
|
||||
|
|
||||
5 | struct MyPath {
|
||||
| ^^^^^^^^^^^^^
|
||||
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `MyPath` type
|
||||
= note: for types from other crates check whether the crate offers a `serde` feature flag
|
||||
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
|
||||
@ -23,8 +53,13 @@ error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satis
|
||||
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
||||
|
|
||||
3 | #[derive(TypedPath)]
|
||||
| ^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`, which is required by `axum::extract::Path<MyPath>: FromRequestParts<S>`
|
||||
| ^^^^^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`
|
||||
--> tests/typed_path/fail/not_deserialize.rs:5:1
|
||||
|
|
||||
5 | struct MyPath {
|
||||
| ^^^^^^^^^^^^^
|
||||
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
|
||||
&'a [u8]
|
||||
&'a serde_json::raw::RawValue
|
||||
@ -37,23 +72,3 @@ error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satis
|
||||
and $N others
|
||||
= note: required for `MyPath` to implement `serde_core::de::DeserializeOwned`
|
||||
= note: required for `axum::extract::Path<MyPath>` to implement `FromRequestParts<S>`
|
||||
|
||||
error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satisfied
|
||||
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
||||
|
|
||||
3 | #[derive(TypedPath)]
|
||||
| ^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`, which is required by `axum::extract::Path<MyPath>: FromRequestParts<S>`
|
||||
|
|
||||
= help: the following other types implement trait `serde_core::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 `MyPath` to implement `serde_core::de::DeserializeOwned`
|
||||
= note: required for `axum::extract::Path<MyPath>` to implement `FromRequestParts<S>`
|
||||
= note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Unreleased
|
||||
|
||||
- **breaking:** Router fallbacks are now properly merged for nested routers ([#3158])
|
||||
- **breaking:** `#[from_request(via(Extractor))]` now uses the extractor's
|
||||
rejection type instead of `axum::response::Response` ([#3261])
|
||||
- **breaking:** `axum::serve` now applies hyper's default `header_read_timeout` ([#3478])
|
||||
- **breaking:** `axum::serve` future output type has been adjusted to remove `io::Result`
|
||||
(never returned `Err`) and be an uninhabited type if `with_graceful_shutdown` is not used
|
||||
(because it was already never terminating if that method wasn't used) ([#3601])
|
||||
- **added:** New `ListenerExt::limit_connections` allows limiting concurrent `axum::serve` connections ([#3489])
|
||||
- **added:** `MethodRouter::method_filter` ([#3586])
|
||||
- **added:** `WebSocketUpgrade::{requested_protocols, set_selected_protocol}` for more
|
||||
flexible subprotocol selection ([#3597])
|
||||
- **changed:** `serve` has an additional generic argument and can now work with any response body
|
||||
type, not just `axum::body::Body` ([#3205])
|
||||
- **changed:** Update minimum rust version to 1.80 ([#3620])
|
||||
- **changed:** `Redirect` constructors now accept any `impl Into<String>` ([#3635])
|
||||
|
||||
[#3158]: https://github.com/tokio-rs/axum/pull/3158
|
||||
[#3261]: https://github.com/tokio-rs/axum/pull/3261
|
||||
[#3205]: https://github.com/tokio-rs/axum/pull/3205
|
||||
[#3478]: https://github.com/tokio-rs/axum/pull/3478
|
||||
[#3601]: https://github.com/tokio-rs/axum/pull/3601
|
||||
[#3489]: https://github.com/tokio-rs/axum/pull/3489
|
||||
[#3586]: https://github.com/tokio-rs/axum/pull/3586
|
||||
[#3597]: https://github.com/tokio-rs/axum/pull/3597
|
||||
[#3620]: https://github.com/tokio-rs/axum/pull/3620
|
||||
|
||||
# 0.8.8
|
||||
|
||||
- Clarify documentation for `Router::route_layer` ([#3567])
|
||||
|
||||
[#3567]: https://github.com/tokio-rs/axum/pull/3567
|
||||
|
||||
# 0.8.7
|
||||
|
||||
- Relax implicit `Send` / `Sync` bounds on `RouterAsService`, `RouterIntoService` ([#3555])
|
||||
|
||||
120
axum/Cargo.toml
120
axum/Cargo.toml
@ -1,16 +1,42 @@
|
||||
[package]
|
||||
name = "axum"
|
||||
version = "0.8.7" # remember to bump the version that axum-extra depends on
|
||||
version = "0.8.8" # remember to bump the version that axum-extra depends on
|
||||
categories = ["asynchronous", "network-programming", "web-programming::http-server"]
|
||||
description = "Web framework that focuses on ergonomics and modularity"
|
||||
description = "HTTP routing and request handling library that focuses on ergonomics and modularity"
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
homepage = "https://github.com/tokio-rs/axum"
|
||||
keywords = ["http", "web", "framework"]
|
||||
keywords = ["http", "web", "routing"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tokio-rs/axum"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[package.metadata.playground]
|
||||
features = ["http1", "http2", "json", "multipart", "ws"]
|
||||
|
||||
[package.metadata.cargo_check_external_types]
|
||||
allowed_external_types = [
|
||||
# our crates
|
||||
"axum_core::*",
|
||||
"axum_macros::*",
|
||||
# not 1.0
|
||||
"futures_core::*",
|
||||
"futures_sink::*",
|
||||
"futures_util::*",
|
||||
"tower_layer::*",
|
||||
"tower_service::*",
|
||||
# >=1.0
|
||||
"bytes::*",
|
||||
"http",
|
||||
"http::*",
|
||||
"http_body::*",
|
||||
"serde_core::*",
|
||||
"tokio::*",
|
||||
]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"form",
|
||||
@ -23,7 +49,13 @@ default = [
|
||||
"tower-log",
|
||||
"tracing",
|
||||
]
|
||||
form = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
|
||||
form = [
|
||||
"dep:form_urlencoded",
|
||||
"dep:serde_html_form",
|
||||
"dep:serde_path_to_error",
|
||||
"serde_html_form/ser",
|
||||
"serde_html_form/de",
|
||||
]
|
||||
http1 = ["dep:hyper", "hyper?/http1", "hyper-util?/http1"]
|
||||
http2 = ["dep:hyper", "hyper?/http2", "hyper-util?/http2"]
|
||||
json = ["dep:serde_json", "dep:serde_path_to_error"]
|
||||
@ -31,11 +63,30 @@ macros = ["dep:axum-macros"]
|
||||
matched-path = []
|
||||
multipart = ["dep:multer"]
|
||||
original-uri = []
|
||||
query = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
|
||||
tokio = ["dep:hyper-util", "dep:tokio", "tokio/net", "tokio/rt", "tower/make", "tokio/macros"]
|
||||
query = [
|
||||
"dep:form_urlencoded",
|
||||
"dep:serde_html_form",
|
||||
"dep:serde_path_to_error",
|
||||
"serde_html_form/de",
|
||||
]
|
||||
tokio = [
|
||||
"dep:hyper-util",
|
||||
"dep:tokio",
|
||||
"tokio/net",
|
||||
"tokio/rt",
|
||||
"tower/make",
|
||||
"tokio/macros",
|
||||
]
|
||||
tower-log = ["tower/log"]
|
||||
tracing = ["dep:tracing", "axum-core/tracing"]
|
||||
ws = ["dep:hyper", "tokio", "dep:tokio-tungstenite", "dep:sha1", "dep:base64"]
|
||||
ws = [
|
||||
"dep:hyper",
|
||||
"dep:futures-sink",
|
||||
"tokio",
|
||||
"dep:tokio-tungstenite",
|
||||
"dep:sha1",
|
||||
"dep:base64",
|
||||
]
|
||||
|
||||
__private_docs = [
|
||||
# We re-export some docs from axum-core via #[doc(inline)],
|
||||
@ -52,8 +103,9 @@ __private_docs = [
|
||||
__private = ["tokio", "http1", "dep:reqwest"]
|
||||
|
||||
[dependencies]
|
||||
axum-core = { path = "../axum-core", version = "0.5.5" }
|
||||
bytes = "1.0"
|
||||
axum-core = { path = "../axum-core", version = "0.5.6" }
|
||||
bytes = "1.7"
|
||||
futures-core = "0.3"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||
http = "1.0.0"
|
||||
http-body = "1.0.0"
|
||||
@ -74,13 +126,14 @@ tower-service = "0.3"
|
||||
axum-macros = { path = "../axum-macros", version = "0.5.0", optional = true }
|
||||
base64 = { version = "0.22.1", optional = true }
|
||||
form_urlencoded = { version = "1.1.0", optional = true }
|
||||
hyper = { version = "1.1.0", optional = true }
|
||||
hyper-util = { version = "0.1.3", features = ["tokio", "server", "service"], optional = true }
|
||||
futures-sink = { version = "0.3", optional = true }
|
||||
hyper = { version = "1.4.0", optional = true }
|
||||
hyper-util = { version = "0.1.4", features = ["tokio", "server", "service"], optional = true }
|
||||
multer = { version = "3.0.0", optional = true }
|
||||
reqwest = { version = "0.12", optional = true, default-features = false, features = ["json", "stream", "multipart"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"], optional = true }
|
||||
serde_html_form = { version = "0.4.0", optional = true, default-features = false, features = ["std"] }
|
||||
serde_json = { version = "1.0.29", features = ["raw_value"], optional = true }
|
||||
serde_path_to_error = { version = "0.1.8", optional = true }
|
||||
serde_urlencoded = { version = "0.7", optional = true }
|
||||
sha1 = { version = "0.10", optional = true }
|
||||
tokio = { package = "tokio", version = "1.44", features = ["time"], optional = true }
|
||||
tokio-tungstenite = { version = "0.28.0", optional = true }
|
||||
@ -133,13 +186,13 @@ quickcheck = "1.0"
|
||||
quickcheck_macros = "1.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
|
||||
serde = { version = "1.0.221", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
serde_json = { version = "1.0.29", features = ["raw_value"] }
|
||||
time = { version = "0.3", features = ["serde-human-readable"] }
|
||||
tokio = { package = "tokio", version = "1.44.2", features = ["macros", "rt", "rt-multi-thread", "net", "test-util"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-tungstenite = "0.28.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["json"] }
|
||||
tracing-subscriber = { version = "0.3.20", features = ["json"] }
|
||||
uuid = { version = "1.0", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies.tower]
|
||||
@ -191,43 +244,6 @@ features = [
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[package.metadata.playground]
|
||||
features = [
|
||||
"http1",
|
||||
"http2",
|
||||
"json",
|
||||
"multipart",
|
||||
"ws",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-public-api-crates]
|
||||
allowed = [
|
||||
# our crates
|
||||
"axum_core",
|
||||
"axum_macros",
|
||||
|
||||
# not 1.0
|
||||
"futures_core",
|
||||
"futures_sink",
|
||||
"futures_util",
|
||||
"pin_project_lite",
|
||||
"tower_layer",
|
||||
"tower_service",
|
||||
|
||||
# >=1.0
|
||||
"bytes",
|
||||
"http",
|
||||
"http_body",
|
||||
"serde_core",
|
||||
"tokio",
|
||||
|
||||
# for the `__private` feature
|
||||
"reqwest",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
name = "benches"
|
||||
harness = false
|
||||
|
||||
25
axum/LICENSE
25
axum/LICENSE
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 axum Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
1
axum/LICENSE
Symbolic link
1
axum/LICENSE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@ -1,6 +1,6 @@
|
||||
# axum
|
||||
|
||||
`axum` is a web application framework that focuses on ergonomics and modularity.
|
||||
`axum` is an HTTP routing and request-handling library that focuses on ergonomics and modularity.
|
||||
|
||||
[](https://github.com/tokio-rs/axum/actions/workflows/CI.yml)
|
||||
[](https://crates.io/crates/axum)
|
||||
@ -17,12 +17,19 @@ More information about this crate can be found in the [crate documentation][docs
|
||||
- Take full advantage of the [`tower`] and [`tower-http`] ecosystem of
|
||||
middleware, services, and utilities.
|
||||
|
||||
In particular the last point is what sets `axum` apart from other frameworks.
|
||||
In particular the last point is what sets `axum` apart from other libraries / frameworks.
|
||||
`axum` doesn't have its own middleware system but instead uses
|
||||
[`tower::Service`]. This means `axum` gets timeouts, tracing, compression,
|
||||
authorization, and more, for free. It also enables you to share middleware with
|
||||
applications written using [`hyper`] or [`tonic`].
|
||||
|
||||
## ⚠ Breaking changes ⚠
|
||||
|
||||
We are currently working towards axum 0.9 so the `main` branch contains breaking
|
||||
changes. See the [`0.8.x`] branch for what's released to crates.io.
|
||||
|
||||
[`0.8.x`]: https://github.com/tokio-rs/axum/tree/v0.8.x
|
||||
|
||||
## Usage example
|
||||
|
||||
```rust
|
||||
@ -47,7 +54,7 @@ async fn main() {
|
||||
|
||||
// run our app with hyper, listening globally on port 3000
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
axum::serve(listener, app).await;
|
||||
}
|
||||
|
||||
// basic handler that responds with a static string
|
||||
@ -104,7 +111,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
|
||||
|
||||
## Minimum supported Rust version
|
||||
|
||||
axum's MSRV is 1.78.
|
||||
axum's MSRV is 1.80.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@ -163,9 +163,9 @@ impl BenchmarkBuilder {
|
||||
.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
#[allow(unreachable_code)] // buggy lint, fixed in nightly
|
||||
std::thread::spawn(move || {
|
||||
rt.block_on(axum::serve(listener, app).into_future())
|
||||
.unwrap();
|
||||
rt.block_on(axum::serve(listener, app).into_future());
|
||||
});
|
||||
|
||||
let mut cmd = Command::new("rewrk");
|
||||
|
||||
@ -48,8 +48,8 @@ async fn fallback_two() -> impl IntoResponse { /* ... */ }
|
||||
## Setting the `Allow` header
|
||||
|
||||
By default `MethodRouter` will set the `Allow` header when returning `405 Method
|
||||
Not Allowed`. This is also done when the fallback is used unless the response
|
||||
generated by the fallback already sets the `Allow` header.
|
||||
Not Allowed`. This is also done when the fallback returns `405 Method Not Allowed`
|
||||
unless the response generated by the fallback already sets the `Allow` header.
|
||||
|
||||
This means if you use `fallback` to accept additional methods, you should make
|
||||
sure you set the `Allow` header correctly.
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
Apply a [`tower::Layer`] to the router that will only run if the request matches
|
||||
a route.
|
||||
|
||||
Note that the middleware is only applied to existing routes. So you have to
|
||||
first add your routes (and / or fallback) and then call `route_layer`
|
||||
Note that the middleware is only applied to existing routes. First add your routes and then call `route_layer`
|
||||
afterwards. Additional routes added after `route_layer` is called will not have
|
||||
the middleware added.
|
||||
|
||||
|
||||
@ -208,7 +208,7 @@ use axum::{
|
||||
body::Body,
|
||||
extract::Request,
|
||||
};
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_core::future::BoxFuture;
|
||||
use tower::{Service, Layer};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
@ -557,7 +557,7 @@ let app_with_middleware = middleware.layer(app);
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app_with_middleware.into_make_service()).await.unwrap();
|
||||
axum::serve(listener, app_with_middleware.into_make_service()).await;
|
||||
# };
|
||||
```
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ let app = Router::new().fallback(handler);
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
axum::serve(listener, app).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -56,6 +56,6 @@ async fn handler() {}
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, handler.into_make_service()).await.unwrap();
|
||||
axum::serve(listener, handler.into_make_service()).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -22,7 +22,7 @@ async fn handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -60,7 +60,7 @@ impl Connected<IncomingStream<'_, TcpListener>> for MyConnectInfo {
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<MyConnectInfo>()).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<MyConnectInfo>()).await;
|
||||
# };
|
||||
```
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ async fn main() {
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
|
||||
axum::serve(listener, router).await.unwrap();
|
||||
axum::serve(listener, router).await;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -36,8 +36,7 @@ documentation for more details.
|
||||
It is not possible to create segments that only match some types like numbers or
|
||||
regular expression. You must handle that manually in your handlers.
|
||||
|
||||
[`MatchedPath`](crate::extract::MatchedPath) can be used to extract the matched
|
||||
path rather than the actual path.
|
||||
[`MatchedPath`] can be used to extract the matched path rather than the actual path.
|
||||
|
||||
# Wildcards
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ let routes = Router::new()
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, routes).await.unwrap();
|
||||
axum::serve(listener, routes).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -41,7 +41,7 @@ let routes = routes().with_state(AppState {});
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, routes).await.unwrap();
|
||||
axum::serve(listener, routes).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -64,7 +64,7 @@ let routes = routes(AppState {});
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, routes).await.unwrap();
|
||||
axum::serve(listener, routes).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -91,7 +91,7 @@ let routes = Router::new().nest("/api", routes(AppState {}));
|
||||
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, routes).await.unwrap();
|
||||
axum::serve(listener, routes).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -124,7 +124,7 @@ let router: Router<()> = router.with_state(AppState {});
|
||||
// because it is still missing an `AppState`.
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, router).await.unwrap();
|
||||
axum::serve(listener, router).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -153,7 +153,7 @@ let final_router: Router<()> = string_router.with_state("foo".to_owned());
|
||||
// Since we have a `Router<()>` we can run it.
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, final_router).await.unwrap();
|
||||
axum::serve(listener, final_router).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -179,7 +179,7 @@ let app = routes(AppState {});
|
||||
// but `app` is a `Router<AppState>`
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
axum::serve(listener, app).await;
|
||||
# };
|
||||
```
|
||||
|
||||
@ -202,7 +202,7 @@ let app = routes(AppState {});
|
||||
// We can now call `Router::into_make_service`
|
||||
# async {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
axum::serve(listener, app).await;
|
||||
# };
|
||||
```
|
||||
|
||||
|
||||
@ -30,12 +30,6 @@ pub struct IntoMakeServiceWithConnectInfo<S, C> {
|
||||
_connect_info: PhantomData<fn() -> C>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traits() {
|
||||
use crate::test_helpers::*;
|
||||
assert_send::<IntoMakeServiceWithConnectInfo<(), NotSendSync>>();
|
||||
}
|
||||
|
||||
impl<S, C> IntoMakeServiceWithConnectInfo<S, C> {
|
||||
pub(crate) fn new(svc: S) -> Self {
|
||||
Self {
|
||||
@ -85,28 +79,19 @@ pub trait Connected<T>: Clone + Send + Sync + 'static {
|
||||
#[cfg(all(feature = "tokio", any(feature = "http1", feature = "http2")))]
|
||||
const _: () = {
|
||||
use crate::serve;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
impl Connected<serve::IncomingStream<'_, TcpListener>> for SocketAddr {
|
||||
fn connect_info(stream: serve::IncomingStream<'_, TcpListener>) -> Self {
|
||||
*stream.remote_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, L, F> Connected<serve::IncomingStream<'a, serve::TapIo<L, F>>> for L::Addr
|
||||
impl<L> Connected<serve::IncomingStream<'_, L>> for SocketAddr
|
||||
where
|
||||
L: serve::Listener,
|
||||
L::Addr: Clone + Sync + 'static,
|
||||
F: FnMut(&mut L::Io) + Send + 'static,
|
||||
L: serve::Listener<Addr = Self>,
|
||||
{
|
||||
fn connect_info(stream: serve::IncomingStream<'a, serve::TapIo<L, F>>) -> Self {
|
||||
stream.remote_addr().clone()
|
||||
fn connect_info(stream: serve::IncomingStream<'_, L>) -> Self {
|
||||
*stream.remote_addr()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
impl Connected<SocketAddr> for SocketAddr {
|
||||
fn connect_info(remote_addr: SocketAddr) -> Self {
|
||||
impl Connected<Self> for SocketAddr {
|
||||
fn connect_info(remote_addr: Self) -> Self {
|
||||
remote_addr
|
||||
}
|
||||
}
|
||||
@ -234,8 +219,90 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{routing::get, serve::IncomingStream, test_helpers::TestClient, Router};
|
||||
use tokio::net::TcpListener;
|
||||
use crate::{
|
||||
extract::connect_info::Connected, routing::get, serve::IncomingStream, serve::Listener,
|
||||
test_helpers::TestClient, Router,
|
||||
};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
#[test]
|
||||
fn into_make_service_traits() {
|
||||
use crate::test_helpers::*;
|
||||
assert_send::<IntoMakeServiceWithConnectInfo<(), NotSendSync>>();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::todo)]
|
||||
fn connected_traits() {
|
||||
// Test that the `Connected` trait can be used with custom address and listener types.
|
||||
|
||||
fn create_router() -> Router {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn tcp_listener() -> TcpListener {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CustomAddr(SocketAddr);
|
||||
|
||||
impl Connected<IncomingStream<'_, TcpListener>> for CustomAddr {
|
||||
fn connect_info(_stream: IncomingStream<'_, TcpListener>) -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Connected<IncomingStream<'_, CustomListener>> for CustomAddr {
|
||||
fn connect_info(_stream: IncomingStream<'_, CustomListener>) -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomListener {}
|
||||
|
||||
impl Listener for CustomListener {
|
||||
type Io = TcpStream;
|
||||
type Addr = SocketAddr;
|
||||
|
||||
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn local_addr(&self) -> tokio::io::Result<Self::Addr> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn custom_connected() {
|
||||
let router = create_router();
|
||||
let _ = crate::serve(
|
||||
tcp_listener(),
|
||||
router.into_make_service_with_connect_info::<CustomAddr>(),
|
||||
);
|
||||
}
|
||||
|
||||
fn custom_listener() {
|
||||
let router = create_router();
|
||||
let _ = crate::serve(CustomListener {}, router.into_make_service());
|
||||
}
|
||||
|
||||
fn custom_listener_with_connect() {
|
||||
let router = create_router();
|
||||
let _ = crate::serve(
|
||||
CustomListener {},
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
);
|
||||
}
|
||||
|
||||
fn custom_listener_with_custom_connect() {
|
||||
let router = create_router();
|
||||
let _ = crate::serve(
|
||||
CustomListener {},
|
||||
router.into_make_service_with_connect_info::<CustomAddr>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn socket_addr() {
|
||||
@ -254,8 +321,7 @@ mod tests {
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
});
|
||||
rx.await.unwrap();
|
||||
|
||||
@ -296,8 +362,7 @@ mod tests {
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<MyConnectInfo>(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
});
|
||||
rx.await.unwrap();
|
||||
|
||||
@ -343,8 +408,7 @@ mod tests {
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
});
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
@ -103,9 +103,7 @@ pub(crate) fn set_matched_path_for_request(
|
||||
route_id_to_path: &HashMap<RouteId, Arc<str>>,
|
||||
extensions: &mut http::Extensions,
|
||||
) {
|
||||
let matched_path = if let Some(matched_path) = route_id_to_path.get(&id) {
|
||||
matched_path
|
||||
} else {
|
||||
let Some(matched_path) = route_id_to_path.get(&id) else {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("should always have a matched path for a route id");
|
||||
#[cfg(not(debug_assertions))]
|
||||
|
||||
@ -81,15 +81,11 @@ pub use self::ws::WebSocketUpgrade;
|
||||
|
||||
// this is duplicated in `axum-extra/src/extract/form.rs`
|
||||
pub(super) fn has_content_type(headers: &HeaderMap, expected_content_type: &mime::Mime) -> bool {
|
||||
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
||||
content_type
|
||||
} else {
|
||||
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let content_type = if let Ok(content_type) = content_type.to_str() {
|
||||
content_type
|
||||
} else {
|
||||
let Ok(content_type) = content_type.to_str() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ use axum_core::{
|
||||
response::{IntoResponse, Response},
|
||||
RequestExt,
|
||||
};
|
||||
use futures_util::stream::Stream;
|
||||
use futures_core::Stream;
|
||||
use http::{
|
||||
header::{HeaderMap, CONTENT_TYPE},
|
||||
StatusCode,
|
||||
|
||||
@ -47,6 +47,7 @@ impl NestedPath {
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend] // pretty niche type
|
||||
impl<S> FromRequestParts<S> for NestedPath
|
||||
where
|
||||
S: Send + Sync,
|
||||
|
||||
@ -76,7 +76,7 @@ where
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let uri = Extension::<Self>::from_request_parts(parts, state)
|
||||
.await
|
||||
.unwrap_or_else(|_| Extension(OriginalUri(parts.uri.clone())))
|
||||
.unwrap_or_else(|_| Extension(Self(parts.uri.clone())))
|
||||
.0;
|
||||
Ok(uri)
|
||||
}
|
||||
|
||||
@ -639,8 +639,7 @@ enum KeyOrIdx<'de> {
|
||||
impl<'de> KeyOrIdx<'de> {
|
||||
fn key(&self) -> &'de str {
|
||||
match &self {
|
||||
Self::Key(key) => key,
|
||||
Self::Idx { key, .. } => key,
|
||||
Self::Idx { key, .. } | Self::Key(key) => key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,7 +183,7 @@ where
|
||||
}
|
||||
|
||||
match T::deserialize(de::PathDeserializer::new(get_params(parts)?)) {
|
||||
Ok(val) => Ok(Path(val)),
|
||||
Ok(val) => Ok(Self(val)),
|
||||
Err(e) => Err(failed_to_deserialize_path_params(e)),
|
||||
}
|
||||
}
|
||||
@ -357,9 +357,9 @@ pub enum ErrorKind {
|
||||
impl fmt::Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ErrorKind::Message(error) => error.fmt(f),
|
||||
ErrorKind::InvalidUtf8InPathParam { key } => write!(f, "Invalid UTF-8 in `{key}`"),
|
||||
ErrorKind::WrongNumberOfParameters { got, expected } => {
|
||||
Self::Message(error) => error.fmt(f),
|
||||
Self::InvalidUtf8InPathParam { key } => write!(f, "Invalid UTF-8 in `{key}`"),
|
||||
Self::WrongNumberOfParameters { got, expected } => {
|
||||
write!(
|
||||
f,
|
||||
"Wrong number of path arguments for `Path`. Expected {expected} but got {got}"
|
||||
@ -371,8 +371,8 @@ impl fmt::Display for ErrorKind {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
ErrorKind::UnsupportedType { name } => write!(f, "Unsupported type `{name}`"),
|
||||
ErrorKind::ParseErrorAtKey {
|
||||
Self::UnsupportedType { name } => write!(f, "Unsupported type `{name}`"),
|
||||
Self::ParseErrorAtKey {
|
||||
key,
|
||||
value,
|
||||
expected_type,
|
||||
@ -380,11 +380,11 @@ impl fmt::Display for ErrorKind {
|
||||
f,
|
||||
"Cannot parse `{key}` with value `{value}` to a `{expected_type}`"
|
||||
),
|
||||
ErrorKind::ParseError {
|
||||
Self::ParseError {
|
||||
value,
|
||||
expected_type,
|
||||
} => write!(f, "Cannot parse `{value}` to a `{expected_type}`"),
|
||||
ErrorKind::ParseErrorAtIndex {
|
||||
Self::ParseErrorAtIndex {
|
||||
index,
|
||||
value,
|
||||
expected_type,
|
||||
@ -392,7 +392,7 @@ impl fmt::Display for ErrorKind {
|
||||
f,
|
||||
"Cannot parse value at index {index} with value `{value}` to a `{expected_type}`"
|
||||
),
|
||||
ErrorKind::DeserializeError {
|
||||
Self::DeserializeError {
|
||||
key,
|
||||
value,
|
||||
message,
|
||||
@ -772,7 +772,7 @@ mod tests {
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = <&str as serde::Deserialize>::deserialize(deserializer)?;
|
||||
Ok(Param(s.to_owned()))
|
||||
Ok(Self(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,16 +36,6 @@ use serde_core::de::DeserializeOwned;
|
||||
///
|
||||
/// If the query string cannot be parsed it will reject the request with a `400
|
||||
/// Bad Request` response.
|
||||
///
|
||||
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
|
||||
/// example.
|
||||
///
|
||||
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
|
||||
///
|
||||
/// For handling multiple values for the same query parameter, in a `?foo=1&foo=2&foo=3`
|
||||
/// fashion, use [`axum_extra::extract::Query`] instead.
|
||||
///
|
||||
/// [`axum_extra::extract::Query`]: https://docs.rs/axum-extra/latest/axum_extra/extract/struct.Query.html
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Query<T>(pub T);
|
||||
@ -88,10 +78,10 @@ where
|
||||
pub fn try_from_uri(value: &Uri) -> Result<Self, QueryRejection> {
|
||||
let query = value.query().unwrap_or_default();
|
||||
let deserializer =
|
||||
serde_urlencoded::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
|
||||
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
|
||||
let params = serde_path_to_error::deserialize(deserializer)
|
||||
.map_err(FailedToDeserializeQueryString::from_err)?;
|
||||
Ok(Query(params))
|
||||
Ok(Self(params))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -157,7 +157,7 @@ use std::{
|
||||
///
|
||||
/// # async {
|
||||
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
/// axum::serve(listener, handler_with_state.into_make_service()).await.unwrap();
|
||||
/// axum::serve(listener, handler_with_state.into_make_service()).await;
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
|
||||
@ -94,10 +94,9 @@ use self::rejection::*;
|
||||
use super::FromRequestParts;
|
||||
use crate::{body::Bytes, response::Response, Error};
|
||||
use axum_core::body::Body;
|
||||
use futures_util::{
|
||||
sink::{Sink, SinkExt},
|
||||
stream::{FusedStream, Stream, StreamExt},
|
||||
};
|
||||
use futures_core::{FusedStream, Stream};
|
||||
use futures_sink::Sink;
|
||||
use futures_util::{sink::SinkExt, stream::StreamExt};
|
||||
use http::{
|
||||
header::{self, HeaderMap, HeaderName, HeaderValue},
|
||||
request::Parts,
|
||||
@ -107,8 +106,10 @@ use hyper_util::rt::TokioIo;
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeSet,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
str,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
use tokio_tungstenite::{
|
||||
@ -138,7 +139,7 @@ pub struct WebSocketUpgrade<F = DefaultOnFailedUpgrade> {
|
||||
sec_websocket_key: Option<HeaderValue>,
|
||||
on_upgrade: hyper::upgrade::OnUpgrade,
|
||||
on_failed_upgrade: F,
|
||||
sec_websocket_protocol: Option<HeaderValue>,
|
||||
sec_websocket_protocol: BTreeSet<HeaderValue>,
|
||||
}
|
||||
|
||||
impl<F> std::fmt::Debug for WebSocketUpgrade<F> {
|
||||
@ -242,35 +243,61 @@ impl<F> WebSocketUpgrade<F> {
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Cow<'static, str>>,
|
||||
{
|
||||
if let Some(req_protocols) = self
|
||||
.sec_websocket_protocol
|
||||
.as_ref()
|
||||
.and_then(|p| p.to_str().ok())
|
||||
{
|
||||
self.protocol = protocols
|
||||
.into_iter()
|
||||
// FIXME: This will often allocate a new `String` and so is less efficient than it
|
||||
// could be. But that can't be fixed without breaking changes to the public API.
|
||||
.map(Into::into)
|
||||
.find(|protocol| {
|
||||
req_protocols
|
||||
.split(',')
|
||||
.any(|req_protocol| req_protocol.trim() == protocol)
|
||||
})
|
||||
.map(|protocol| match protocol {
|
||||
Cow::Owned(s) => HeaderValue::from_str(&s).unwrap(),
|
||||
Cow::Borrowed(s) => HeaderValue::from_static(s),
|
||||
});
|
||||
}
|
||||
self.protocol = protocols
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.find(|proto| {
|
||||
// FIXME: When https://github.com/hyperium/http/pull/814
|
||||
// is merged + released, we can look use
|
||||
// `contains(proto.as_bytes())` without converting
|
||||
// to `HeaderValue` first.
|
||||
let Ok(proto) = HeaderValue::from_str(proto) else {
|
||||
return false;
|
||||
};
|
||||
self.sec_websocket_protocol.contains(&proto)
|
||||
})
|
||||
.map(|protocol| match protocol {
|
||||
Cow::Owned(s) => HeaderValue::from_str(&s).unwrap(),
|
||||
Cow::Borrowed(s) => HeaderValue::from_static(s),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the WebSocket subprotocols requested by the client.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// If the client sends the following HTTP header in the WebSocket upgrade request:
|
||||
///
|
||||
/// ```txt
|
||||
/// Sec-WebSocket-Protocol: soap, wamp
|
||||
/// ```
|
||||
///
|
||||
/// this method returns an iterator yielding `"soap"` and `"wamp"`.
|
||||
pub fn requested_protocols(&self) -> impl Iterator<Item = &HeaderValue> {
|
||||
self.sec_websocket_protocol.iter()
|
||||
}
|
||||
|
||||
/// Set the chosen WebSocket subprotocol.
|
||||
///
|
||||
/// Another method, [`protocols()`][Self::protocols], also sets the chosen WebSocket
|
||||
/// subprotocol. If both methods are called, only the latter call takes effect.
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// - The chosen protocol is echoed back in the WebSocket upgrade
|
||||
/// response as required by RFC 6455. Some browsers may reject a
|
||||
/// value that was not present in the client's request.
|
||||
pub fn set_selected_protocol(&mut self, protocol: HeaderValue) {
|
||||
self.protocol = Some(protocol);
|
||||
}
|
||||
|
||||
/// Return the selected WebSocket subprotocol, if one has been chosen.
|
||||
///
|
||||
/// If [`protocols()`][Self::protocols] has been called and a matching
|
||||
/// protocol has been selected, the return value will be `Some` containing
|
||||
/// said protocol. Otherwise, it will be `None`.
|
||||
/// If [`protocols()`][Self::protocols] selects a matching protocol, or
|
||||
/// [`set_selected_protocol()`][Self::set_selected_protocol] has been called, the return
|
||||
/// value will be `Some` containing the selected protocol. Otherwise, it will be `None`.
|
||||
pub fn selected_protocol(&self) -> Option<&HeaderValue> {
|
||||
self.protocol.as_ref()
|
||||
}
|
||||
@ -424,11 +451,11 @@ where
|
||||
return Err(MethodNotGet.into());
|
||||
}
|
||||
|
||||
if !header_contains(&parts.headers, header::CONNECTION, "upgrade") {
|
||||
if !header_contains(&parts.headers, &header::CONNECTION, "upgrade") {
|
||||
return Err(InvalidConnectionHeader.into());
|
||||
}
|
||||
|
||||
if !header_eq(&parts.headers, header::UPGRADE, "websocket") {
|
||||
if !header_eq(&parts.headers, &header::UPGRADE, "websocket") {
|
||||
return Err(InvalidUpgradeHeader.into());
|
||||
}
|
||||
|
||||
@ -458,7 +485,7 @@ where
|
||||
None
|
||||
};
|
||||
|
||||
if !header_eq(&parts.headers, header::SEC_WEBSOCKET_VERSION, "13") {
|
||||
if !header_eq(&parts.headers, &header::SEC_WEBSOCKET_VERSION, "13") {
|
||||
return Err(InvalidWebSocketVersionHeader.into());
|
||||
}
|
||||
|
||||
@ -467,7 +494,16 @@ where
|
||||
.remove::<hyper::upgrade::OnUpgrade>()
|
||||
.ok_or(ConnectionNotUpgradable)?;
|
||||
|
||||
let sec_websocket_protocol = parts.headers.get(header::SEC_WEBSOCKET_PROTOCOL).cloned();
|
||||
let sec_websocket_protocol = parts
|
||||
.headers
|
||||
.get_all(header::SEC_WEBSOCKET_PROTOCOL)
|
||||
.iter()
|
||||
.flat_map(|val| val.as_bytes().split(|&b| b == b','))
|
||||
.map(|proto| {
|
||||
HeaderValue::from_bytes(proto.trim_ascii())
|
||||
.expect("substring of HeaderValue is valid HeaderValue")
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
config: Default::default(),
|
||||
@ -480,18 +516,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn header_eq(headers: &HeaderMap, key: HeaderName, value: &'static str) -> bool {
|
||||
if let Some(header) = headers.get(&key) {
|
||||
fn header_eq(headers: &HeaderMap, key: &HeaderName, value: &'static str) -> bool {
|
||||
if let Some(header) = headers.get(key) {
|
||||
header.as_bytes().eq_ignore_ascii_case(value.as_bytes())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn header_contains(headers: &HeaderMap, key: HeaderName, value: &'static str) -> bool {
|
||||
let header = if let Some(header) = headers.get(&key) {
|
||||
header
|
||||
} else {
|
||||
fn header_contains(headers: &HeaderMap, key: &HeaderName, value: &'static str) -> bool {
|
||||
let Some(header) = headers.get(key) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@ -842,49 +876,49 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Create a new text WebSocket message from a stringable.
|
||||
pub fn text<S>(string: S) -> Message
|
||||
pub fn text<S>(string: S) -> Self
|
||||
where
|
||||
S: Into<Utf8Bytes>,
|
||||
{
|
||||
Message::Text(string.into())
|
||||
Self::Text(string.into())
|
||||
}
|
||||
|
||||
/// Create a new binary WebSocket message by converting to `Bytes`.
|
||||
pub fn binary<B>(bin: B) -> Message
|
||||
pub fn binary<B>(bin: B) -> Self
|
||||
where
|
||||
B: Into<Bytes>,
|
||||
{
|
||||
Message::Binary(bin.into())
|
||||
Self::Binary(bin.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Message {
|
||||
fn from(string: String) -> Self {
|
||||
Message::Text(string.into())
|
||||
Self::Text(string.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> From<&'s str> for Message {
|
||||
fn from(string: &'s str) -> Self {
|
||||
Message::Text(string.into())
|
||||
Self::Text(string.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> From<&'b [u8]> for Message {
|
||||
fn from(data: &'b [u8]) -> Self {
|
||||
Message::Binary(Bytes::copy_from_slice(data))
|
||||
Self::Binary(Bytes::copy_from_slice(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for Message {
|
||||
fn from(data: Bytes) -> Self {
|
||||
Message::Binary(data)
|
||||
Self::Binary(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Message {
|
||||
fn from(data: Vec<u8>) -> Self {
|
||||
Message::Binary(data.into())
|
||||
Self::Binary(data.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::extract::Request;
|
||||
use crate::extract::{rejection::*, FromRequest, RawForm};
|
||||
use axum_core::response::{IntoResponse, Response};
|
||||
use axum_core::response::{IntoResponse, IntoResponseFailed, Response};
|
||||
use axum_core::RequestExt;
|
||||
use http::header::CONTENT_TYPE;
|
||||
use http::StatusCode;
|
||||
@ -84,7 +84,7 @@ where
|
||||
match req.extract().await {
|
||||
Ok(RawForm(bytes)) => {
|
||||
let deserializer =
|
||||
serde_urlencoded::Deserializer::new(form_urlencoded::parse(&bytes));
|
||||
serde_html_form::Deserializer::new(form_urlencoded::parse(&bytes));
|
||||
let value = serde_path_to_error::deserialize(deserializer).map_err(
|
||||
|err| -> FormRejection {
|
||||
if is_get_or_head {
|
||||
@ -94,7 +94,7 @@ where
|
||||
}
|
||||
},
|
||||
)?;
|
||||
Ok(Form(value))
|
||||
Ok(Self(value))
|
||||
}
|
||||
Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)),
|
||||
Err(RawFormRejection::InvalidFormContentType(r)) => {
|
||||
@ -110,18 +110,23 @@ where
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
// Extracted into separate fn so it's only compiled once for all T.
|
||||
fn make_response(ser_result: Result<String, serde_urlencoded::ser::Error>) -> Response {
|
||||
fn make_response(ser_result: Result<String, serde_html_form::ser::Error>) -> Response {
|
||||
match ser_result {
|
||||
Ok(body) => (
|
||||
[(CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref())],
|
||||
body,
|
||||
)
|
||||
.into_response(),
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
IntoResponseFailed,
|
||||
err.to_string(),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
make_response(serde_urlencoded::to_string(&self.0))
|
||||
make_response(serde_html_form::to_string(&self.0))
|
||||
}
|
||||
}
|
||||
axum_core::__impl_deref!(Form);
|
||||
@ -160,7 +165,7 @@ mod tests {
|
||||
.uri("http://example.com/test")
|
||||
.method(Method::POST)
|
||||
.header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
|
||||
.body(Body::from(serde_urlencoded::to_string(&value).unwrap()))
|
||||
.body(Body::from(serde_html_form::to_string(&value).unwrap()))
|
||||
.unwrap();
|
||||
assert_eq!(Form::<T>::from_request(req, &()).await.unwrap().0, value);
|
||||
}
|
||||
@ -223,7 +228,7 @@ mod tests {
|
||||
.method(Method::POST)
|
||||
.header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
|
||||
.body(Body::from(
|
||||
serde_urlencoded::to_string(&Pagination {
|
||||
serde_html_form::to_string(&Pagination {
|
||||
size: Some(10),
|
||||
page: None,
|
||||
})
|
||||
|
||||
@ -204,6 +204,7 @@ pub trait Handler<T, S>: Clone + Send + Sync + Sized + 'static {
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend]
|
||||
impl<F, Fut, Res, S> Handler<((),), S> for F
|
||||
where
|
||||
F: FnOnce() -> Fut + Clone + Send + Sync + 'static,
|
||||
@ -221,6 +222,7 @@ macro_rules! impl_handler {
|
||||
(
|
||||
[$($ty:ident),*], $last:ident
|
||||
) => {
|
||||
#[diagnostic::do_not_recommend]
|
||||
#[allow(non_snake_case, unused_mut)]
|
||||
impl<F, Fut, S, Res, M, $($ty,)* $last> Handler<(M, $($ty,)* $last,), S> for F
|
||||
where
|
||||
@ -265,6 +267,7 @@ mod private {
|
||||
pub enum IntoResponseHandler {}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend]
|
||||
impl<T, S> Handler<private::IntoResponseHandler, S> for T
|
||||
where
|
||||
T: IntoResponse + Clone + Send + Sync + 'static,
|
||||
@ -310,6 +313,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::do_not_recommend]
|
||||
impl<H, S, T, L> Handler<T, S> for Layered<L, H, T, S>
|
||||
where
|
||||
L: Layer<HandlerService<H, T, S>> + Clone + Send + Sync + 'static,
|
||||
|
||||
@ -55,12 +55,12 @@ impl<H, T, S> HandlerService<H, T, S> {
|
||||
///
|
||||
/// # async {
|
||||
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
/// axum::serve(listener, app.into_make_service()).await.unwrap();
|
||||
/// axum::serve(listener, app.into_make_service()).await;
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// [`MakeService`]: tower::make::MakeService
|
||||
pub fn into_make_service(self) -> IntoMakeService<HandlerService<H, T, S>> {
|
||||
pub fn into_make_service(self) -> IntoMakeService<Self> {
|
||||
IntoMakeService::new(self)
|
||||
}
|
||||
|
||||
@ -94,16 +94,14 @@ impl<H, T, S> HandlerService<H, T, S> {
|
||||
/// axum::serve(
|
||||
/// listener,
|
||||
/// app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
/// ).await.unwrap();
|
||||
/// ).await;
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// [`MakeService`]: tower::make::MakeService
|
||||
/// [`Router::into_make_service_with_connect_info`]: crate::routing::Router::into_make_service_with_connect_info
|
||||
#[cfg(feature = "tokio")]
|
||||
pub fn into_make_service_with_connect_info<C>(
|
||||
self,
|
||||
) -> IntoMakeServiceWithConnectInfo<HandlerService<H, T, S>, C> {
|
||||
pub fn into_make_service_with_connect_info<C>(self) -> IntoMakeServiceWithConnectInfo<Self, C> {
|
||||
IntoMakeServiceWithConnectInfo::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::extract::Request;
|
||||
use crate::extract::{rejection::*, FromRequest};
|
||||
use axum_core::extract::OptionalFromRequest;
|
||||
use axum_core::response::{IntoResponse, Response};
|
||||
use axum_core::response::{IntoResponse, IntoResponseFailed, Response};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use http::{
|
||||
header::{self, HeaderMap, HeaderValue},
|
||||
@ -136,22 +136,14 @@ where
|
||||
}
|
||||
|
||||
fn json_content_type(headers: &HeaderMap) -> bool {
|
||||
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(content_type) = content_type.to_str() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(mime) = content_type.parse::<mime::Mime>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let is_json_content_type = mime.type_() == "application"
|
||||
&& (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"));
|
||||
|
||||
is_json_content_type
|
||||
headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|content_type| content_type.to_str().ok())
|
||||
.and_then(|content_type| content_type.parse::<mime::Mime>().ok())
|
||||
.is_some_and(|mime| {
|
||||
mime.type_() == "application"
|
||||
&& (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"))
|
||||
})
|
||||
}
|
||||
|
||||
axum_core::__impl_deref!(Json);
|
||||
@ -224,6 +216,7 @@ where
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
|
||||
)],
|
||||
IntoResponseFailed,
|
||||
err.to_string(),
|
||||
)
|
||||
.into_response(),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//! axum is a web application framework that focuses on ergonomics and modularity.
|
||||
//! axum is an HTTP routing and request-handling library that focuses on ergonomics and modularity.
|
||||
//!
|
||||
//! # High-level features
|
||||
//!
|
||||
@ -9,7 +9,7 @@
|
||||
//! - Take full advantage of the [`tower`] and [`tower-http`] ecosystem of
|
||||
//! middleware, services, and utilities.
|
||||
//!
|
||||
//! In particular, the last point is what sets `axum` apart from other frameworks.
|
||||
//! In particular, the last point is what sets `axum` apart from other libraries / frameworks.
|
||||
//! `axum` doesn't have its own middleware system but instead uses
|
||||
//! [`tower::Service`]. This means `axum` gets timeouts, tracing, compression,
|
||||
//! authorization, and more, for free. It also enables you to share middleware with
|
||||
@ -37,7 +37,7 @@
|
||||
//!
|
||||
//! // run our app with hyper, listening globally on port 3000
|
||||
//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
//! axum::serve(listener, app).await.unwrap();
|
||||
//! axum::serve(listener, app).await;
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@ -455,6 +455,7 @@ pub mod serve;
|
||||
|
||||
#[cfg(any(test, feature = "__private"))]
|
||||
#[allow(missing_docs, missing_debug_implementations, clippy::print_stdout)]
|
||||
#[doc(hidden)]
|
||||
pub mod test_helpers;
|
||||
|
||||
#[doc(no_inline)]
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::{
|
||||
extract::FromRequestParts,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_core::future::BoxFuture;
|
||||
use http::Request;
|
||||
use pin_project_lite::pin_project;
|
||||
use std::{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user