mirror of
https://github.com/tokio-rs/axum.git
synced 2025-03-13 11:18:33 +01:00
Merge branch 'main' into impl-status
This commit is contained in:
commit
daeb2c7f0d
237 changed files with 5598 additions and 2547 deletions
21
.github/DISCUSSION_TEMPLATE/q-a.yml
vendored
Normal file
21
.github/DISCUSSION_TEMPLATE/q-a.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: 'Your question:'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: axum version
|
||||
description: 'Please look it up in `Cargo.lock`, or as described below'
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> If you have `jq` installed, you can look up the version by running
|
||||
>
|
||||
> ```bash
|
||||
> cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "axum") | .version'
|
||||
> ```
|
43
.github/workflows/CI.yml
vendored
43
.github/workflows/CI.yml
vendored
|
@ -2,7 +2,7 @@ name: CI
|
|||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
MSRV: '1.63'
|
||||
MSRV: '1.66'
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -14,7 +14,7 @@ jobs:
|
|||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@protoc
|
||||
- uses: dtolnay/rust-toolchain@beta
|
||||
with:
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
check-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: cargo doc
|
||||
|
@ -39,7 +39,7 @@ jobs:
|
|||
cargo-hack:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@protoc
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
@ -55,14 +55,20 @@ jobs:
|
|||
matrix:
|
||||
crate: [axum, axum-core, axum-extra, axum-macros]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: actions/checkout@v4
|
||||
# Pinned version due to failing `cargo-public-api-crates`.
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-06
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install cargo-public-api-crates
|
||||
run: |
|
||||
cargo install --git https://github.com/davidpdrsn/cargo-public-api-crates
|
||||
- name: Build rustdoc
|
||||
run: |
|
||||
cargo rustdoc --all-features --manifest-path ${{ matrix.crate }}/Cargo.toml -- -Z unstable-options --output-format json
|
||||
- name: cargo public-api-crates check
|
||||
run: cargo public-api-crates --manifest-path ${{ matrix.crate }}/Cargo.toml check
|
||||
run: cargo public-api-crates --manifest-path ${{ matrix.crate }}/Cargo.toml --skip-build check
|
||||
|
||||
test-versions:
|
||||
needs: check
|
||||
|
@ -71,7 +77,7 @@ jobs:
|
|||
matrix:
|
||||
rust: [stable, beta]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@protoc
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
|
@ -85,7 +91,7 @@ jobs:
|
|||
needs: check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get rust-toolchain version
|
||||
id: rust-toolchain
|
||||
run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT
|
||||
|
@ -103,7 +109,7 @@ jobs:
|
|||
needs: check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.MSRV }}
|
||||
|
@ -122,7 +128,6 @@ jobs:
|
|||
-p axum-extra
|
||||
-p axum-core
|
||||
--all-features
|
||||
--all-targets
|
||||
--locked
|
||||
# the compiler errors are different on our MSRV which makes
|
||||
# the trybuild tests in axum-macros fail, so just run the doc
|
||||
|
@ -140,7 +145,7 @@ jobs:
|
|||
needs: check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run doc tests
|
||||
|
@ -156,17 +161,17 @@ jobs:
|
|||
- advisories
|
||||
- bans licenses sources
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
arguments: --all-features --manifest-path axum/Cargo.toml
|
||||
manifest-path: axum/Cargo.toml
|
||||
|
||||
armv5te-unknown-linux-musleabi:
|
||||
needs: check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: armv5te-unknown-linux-musleabi
|
||||
|
@ -190,7 +195,7 @@ jobs:
|
|||
needs: check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-unknown-unknown
|
||||
|
@ -205,7 +210,7 @@ jobs:
|
|||
dependencies-are-sorted:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@beta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install cargo-sort
|
||||
|
@ -225,7 +230,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.16.2
|
||||
uses: crate-ci/typos@v1.20.8
|
||||
|
|
|
@ -267,7 +267,7 @@ If a Pull Request appears to be abandoned or stalled, it is polite to first
|
|||
check with the contributor to see if they intend to continue the work before
|
||||
checking if they would mind if you took it over (especially if it just has nits
|
||||
left). When doing so, it is courteous to give the original contributor credit
|
||||
for the work they started (either by preserving their name and email address in
|
||||
for the work they started, either by preserving their name and email address in
|
||||
the commit log, or by using an `Author: ` meta-data tag in the commit.
|
||||
|
||||
[hiding-a-comment]: https://help.github.com/articles/managing-disruptive-comments/#hiding-a-comment
|
||||
|
|
17
ECOSYSTEM.md
17
ECOSYSTEM.md
|
@ -16,16 +16,17 @@ If your project isn't listed here and you would like it to be, please feel free
|
|||
- [axum_session_auth](https://github.com/AscendingCreations/AxumSessionsAuth): Persistent session based user login with rights management for Axum.
|
||||
- [axum-auth](https://crates.io/crates/axum-auth): High-level http auth extractors for axum.
|
||||
- [axum-keycloak-auth](https://github.com/lpotthast/axum-keycloak-auth): Protect axum routes with a JWT emitted by Keycloak.
|
||||
- [shuttle](https://github.com/getsynth/shuttle): A serverless platform built for Rust. Now with axum support.
|
||||
- [axum-tungstenite](https://github.com/davidpdrsn/axum-tungstenite): WebSocket connections for axum directly using tungstenite
|
||||
- [axum-jrpc](https://github.com/0xdeafbeef/axum-jrpc): Json-rpc extractor for axum
|
||||
- [axum-tracing-opentelemetry](https://crates.io/crates/axum-tracing-opentelemetry): Middlewares and tools to integrate axum + tracing + opentelemetry
|
||||
- [svelte-axum-project](https://github.com/jbertovic/svelte-axum-project): Template and example for Svelte frontend app with Axum as backend
|
||||
- [axum-streams](https://github.com/abdolence/axum-streams-rs): Streaming HTTP body with different formats: JSON, CSV, Protobuf.
|
||||
- [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
|
||||
- [aide](https://docs.rs/aide): Code-first Open API documentation generator with [axum integration](https://docs.rs/aide/latest/aide/axum/index.html).
|
||||
- [axum-typed-routing](https://docs.rs/axum-typed-routing/latest/axum_typed_routing/): Statically typed routing macros with OpenAPI generation using aide.
|
||||
- [axum-jsonschema](https://docs.rs/axum-jsonschema/): A `Json<T>` extractor that does JSON schema validation of requests.
|
||||
- [axum-sessions](https://docs.rs/axum-sessions): Cookie-based sessions for axum via async-session.
|
||||
- [axum-login](https://docs.rs/axum-login): Session-based user authentication for axum.
|
||||
|
@ -38,6 +39,15 @@ If your project isn't listed here and you would like it to be, please feel free
|
|||
- [springtime-web-axum](https://crates.io/crates/springtime-web-axum): A web framework built on Springtime and axum, leveraging dependency injection for easy app development.
|
||||
- [rust-axum-with-google-oauth](https://github.com/randommm/rust-axum-with-google-oauth): website template for Google OAuth authentication on Axum, using SQLite with SQLx or MongoDB and MiniJinja.
|
||||
- [axum-htmx](https://github.com/robertwayne/axum-htmx): Htmx extractors and request guards for axum.
|
||||
- [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.
|
||||
- [axum-test](https://crates.io/crates/axum-test): High level library for writing Cargo tests that run against Axum.
|
||||
- [axum-messages](https://github.com/maxcountryman/axum-messages): One-time notification messages for Axum.
|
||||
|
||||
## Project showcase
|
||||
|
||||
|
@ -51,6 +61,7 @@ If your project isn't listed here and you would like it to be, please feel free
|
|||
- [realworld-axum-sqlx](https://github.com/launchbadge/realworld-axum-sqlx): A Rust implementation of the [Realworld] demo app spec using Axum and [SQLx].
|
||||
See https://github.com/davidpdrsn/realworld-axum-sqlx for a fork with up to date dependencies.
|
||||
- [Rustapi](https://github.com/ndelvalle/rustapi): RESTful API template using MongoDB
|
||||
- [RUSTfulapi](https://github.com/robatipoor/rustfulapi): Reusable template for building REST Web Services in Rust. Uses Axum HTTP web framework and SeaORM.
|
||||
- [Jotsy](https://github.com/ohsayan/jotsy): Self-hosted notes app powered by Skytable, Axum and Tokio
|
||||
- [Svix](https://www.svix.com) ([repository](https://github.com/svix/svix-webhooks)): Enterprise-ready webhook service
|
||||
- [emojied](https://emojied.net) ([repository](https://github.com/sekunho/emojied)): Shorten URLs to emojis!
|
||||
|
@ -73,6 +84,8 @@ If your project isn't listed here and you would like it to be, please feel free
|
|||
- [httq](https://github.com/scotow/httq) HTTP to MQTT trivial proxy.
|
||||
- [ReductStore](https://github.com/reductstore/reductstore): A time series database for storing and managing large amounts of blob data
|
||||
- [randoku](https://github.com/stchris/randoku): A tiny web service which generates random numbers and shuffles lists randomly
|
||||
- [sero](https://github.com/clowzed/sero): Host static sites with custom subdomains as surge.sh does. But with full control and cool new features. (axum, sea-orm, postgresql)
|
||||
- [Hatsu](https://github.com/importantimport/hatsu): 🩵 Self-hosted & Fully-automated ActivityPub Bridge for Static Sites.
|
||||
|
||||
[Realworld]: https://github.com/gothinkster/realworld
|
||||
[SQLx]: https://github.com/launchbadge/sqlx
|
||||
|
@ -87,6 +100,7 @@ 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]
|
||||
- [Introduction to axum]: YouTube playlist
|
||||
- [Rust Axum Full Course]: YouTube video
|
||||
- [Deploying Axum projects with Shuttle]
|
||||
|
||||
[axum-tutorial]: https://github.com/programatik29/axum-tutorial
|
||||
[axum-tutorial-website]: https://programatik29.github.io/axum-tutorial/
|
||||
|
@ -96,4 +110,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
|
||||
[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.
|
||||
|
|
6
_typos.toml
Normal file
6
_typos.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[files]
|
||||
extend-exclude = ["Cargo.toml"]
|
||||
|
||||
[default.extend-identifiers]
|
||||
DefaultOnFailedUpdgrade = "DefaultOnFailedUpdgrade"
|
||||
OnFailedUpdgrade = "OnFailedUpdgrade"
|
|
@ -9,6 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- None.
|
||||
|
||||
# 0.4.3 (13. January, 2024)
|
||||
|
||||
- **added:** Implement `IntoResponseParts` for `()` ([#2471])
|
||||
|
||||
[#2471]: https://github.com/tokio-rs/axum/pull/2471
|
||||
|
||||
# 0.4.2 (29. December, 2023)
|
||||
|
||||
- **added:** `Body` implements `From<()>` now ([#2411])
|
||||
|
||||
[#2411]: https://github.com/tokio-rs/axum/pull/2411
|
||||
|
||||
# 0.4.1 (03. December, 2023)
|
||||
|
||||
- Fix from_stream doc link to `Stream` in docs ([#2391])
|
||||
|
||||
[#2391]: https://github.com/tokio-rs/axum/pull/2391
|
||||
|
||||
# 0.4.0 (27. November, 2023)
|
||||
|
||||
- **added:** Implement `IntoResponse` for `(R,) where R: IntoResponse` ([#2143])
|
||||
- **fixed:** Fix broken docs links ([#2164])
|
||||
- **fixed:** Clearly document applying `DefaultBodyLimit` to individual routes ([#2157])
|
||||
- **breaking:** The following types/traits are no longer generic over the request body
|
||||
(i.e. the `B` type param has been removed) ([#1751] and [#1789]):
|
||||
- `FromRequestParts`
|
||||
- `FromRequest`
|
||||
- `RequestExt`
|
||||
- **breaking:** axum no longer re-exports `hyper::Body` as that type is removed
|
||||
in hyper 1.0. Instead axum has its own body type at `axum_core::body::Body` ([#1751])
|
||||
|
||||
[#2143]: https://github.com/tokio-rs/axum/pull/2143
|
||||
[#2164]: https://github.com/tokio-rs/axum/pull/2164
|
||||
[#2157]: https://github.com/tokio-rs/axum/pull/2157
|
||||
|
||||
# 0.3.4 (11. April, 2023)
|
||||
|
||||
- Changes to private APIs.
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
categories = ["asynchronous", "network-programming", "web-programming"]
|
||||
description = "Core types and traits for axum"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
rust-version = "1.57"
|
||||
homepage = "https://github.com/tokio-rs/axum"
|
||||
keywords = ["http", "web", "framework"]
|
||||
license = "MIT"
|
||||
name = "axum-core"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tokio-rs/axum"
|
||||
version = "0.3.4" # remember to also bump the version that axum and axum-extra depend on
|
||||
version = "0.4.3" # remember to also bump the version that axum and axum-extra depend on
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing"]
|
||||
|
@ -21,34 +21,40 @@ __private_docs = ["dep:tower-http"]
|
|||
async-trait = "0.1.67"
|
||||
bytes = "1.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.7"
|
||||
http-body = "0.4.5"
|
||||
http = "1.0.0"
|
||||
http-body = "1.0.0"
|
||||
http-body-util = "0.1.0"
|
||||
mime = "0.3.16"
|
||||
pin-project-lite = "0.2.7"
|
||||
sync_wrapper = "0.1.1"
|
||||
rustversion = "1.0.9"
|
||||
sync_wrapper = "1.0.0"
|
||||
tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
|
||||
# optional dependencies
|
||||
tower-http = { version = "0.4", optional = true, features = ["limit"] }
|
||||
tower-http = { version = "0.5.0", optional = true, features = ["limit"] }
|
||||
tracing = { version = "0.1.37", default-features = false, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
rustversion = "1.0.9"
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { path = "../axum", version = "0.6.0" }
|
||||
axum = { path = "../axum", version = "0.7.2" }
|
||||
axum-extra = { path = "../axum-extra", features = ["typed-header"] }
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||
hyper = "0.14.24"
|
||||
hyper = "1.0.0"
|
||||
tokio = { version = "1.25.0", features = ["macros"] }
|
||||
tower-http = { version = "0.4", features = ["limit"] }
|
||||
tower-http = { version = "0.5.0", features = ["limit"] }
|
||||
|
||||
[package.metadata.cargo-public-api-crates]
|
||||
allowed = [
|
||||
# not 1.0
|
||||
"futures_core",
|
||||
"http",
|
||||
"bytes",
|
||||
"http_body",
|
||||
"tower_layer",
|
||||
|
||||
# >=1.0
|
||||
"bytes",
|
||||
"http",
|
||||
"http_body",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#[rustversion::nightly]
|
||||
fn main() {
|
||||
println!("cargo:rustc-cfg=nightly_error_messages");
|
||||
}
|
||||
|
||||
#[rustversion::not(nightly)]
|
||||
fn main() {}
|
|
@ -2,17 +2,16 @@
|
|||
|
||||
use crate::{BoxError, Error};
|
||||
use bytes::Bytes;
|
||||
use bytes::{Buf, BufMut};
|
||||
use futures_util::stream::Stream;
|
||||
use futures_util::TryStream;
|
||||
use http::HeaderMap;
|
||||
use http_body::Body as _;
|
||||
use http_body::{Body as _, Frame};
|
||||
use http_body_util::BodyExt;
|
||||
use pin_project_lite::pin_project;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use sync_wrapper::SyncWrapper;
|
||||
|
||||
type BoxBody = http_body::combinators::UnsyncBoxBody<Bytes, Error>;
|
||||
type BoxBody = http_body_util::combinators::UnsyncBoxBody<Bytes, Error>;
|
||||
|
||||
fn boxed<B>(body: B) -> BoxBody
|
||||
where
|
||||
|
@ -35,58 +34,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
// copied from hyper under the following license:
|
||||
// Copyright (c) 2014-2021 Sean McArthur
|
||||
|
||||
// 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.
|
||||
pub(crate) async fn to_bytes<T>(body: T) -> Result<Bytes, T::Error>
|
||||
where
|
||||
T: http_body::Body,
|
||||
{
|
||||
futures_util::pin_mut!(body);
|
||||
|
||||
// If there's only 1 chunk, we can just return Buf::to_bytes()
|
||||
let mut first = if let Some(buf) = body.data().await {
|
||||
buf?
|
||||
} else {
|
||||
return Ok(Bytes::new());
|
||||
};
|
||||
|
||||
let second = if let Some(buf) = body.data().await {
|
||||
buf?
|
||||
} else {
|
||||
return Ok(first.copy_to_bytes(first.remaining()));
|
||||
};
|
||||
|
||||
// With more than 1 buf, we gotta flatten into a Vec first.
|
||||
let cap = first.remaining() + second.remaining() + body.size_hint().lower() as usize;
|
||||
let mut vec = Vec::with_capacity(cap);
|
||||
vec.put(first);
|
||||
vec.put(second);
|
||||
|
||||
while let Some(buf) = body.data().await {
|
||||
vec.put(buf?);
|
||||
}
|
||||
|
||||
Ok(vec.into())
|
||||
}
|
||||
|
||||
/// The body type used in axum requests and responses.
|
||||
#[derive(Debug)]
|
||||
pub struct Body(BoxBody);
|
||||
|
@ -103,12 +50,12 @@ impl Body {
|
|||
|
||||
/// Create an empty body.
|
||||
pub fn empty() -> Self {
|
||||
Self::new(http_body::Empty::new())
|
||||
Self::new(http_body_util::Empty::new())
|
||||
}
|
||||
|
||||
/// Create a new `Body` from a [`Stream`].
|
||||
///
|
||||
/// [`Stream`]: futures_util::stream::Stream
|
||||
/// [`Stream`]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html
|
||||
pub fn from_stream<S>(stream: S) -> Self
|
||||
where
|
||||
S: TryStream + Send + 'static,
|
||||
|
@ -119,6 +66,16 @@ impl Body {
|
|||
stream: SyncWrapper::new(stream),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert the body into a [`Stream`] of data frames.
|
||||
///
|
||||
/// Non-data frames (such as trailers) will be discarded. Use [`http_body_util::BodyStream`] if
|
||||
/// you need a [`Stream`] of all frame types.
|
||||
///
|
||||
/// [`http_body_util::BodyStream`]: https://docs.rs/http-body-util/latest/http_body_util/struct.BodyStream.html
|
||||
pub fn into_data_stream(self) -> BodyDataStream {
|
||||
BodyDataStream { inner: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Body {
|
||||
|
@ -127,11 +84,17 @@ impl Default for Body {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<()> for Body {
|
||||
fn from(_: ()) -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! body_from_impl {
|
||||
($ty:ty) => {
|
||||
impl From<$ty> for Body {
|
||||
fn from(buf: $ty) -> Self {
|
||||
Self::new(http_body::Full::from(buf))
|
||||
Self::new(http_body_util::Full::from(buf))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -152,19 +115,11 @@ impl http_body::Body for Body {
|
|||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn poll_data(
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> std::task::Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||
Pin::new(&mut self.0).poll_data(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_trailers(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> std::task::Poll<Result<Option<HeaderMap>, Self::Error>> {
|
||||
Pin::new(&mut self.0).poll_trailers(cx)
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
Pin::new(&mut self.0).poll_frame(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -178,12 +133,51 @@ impl http_body::Body for Body {
|
|||
}
|
||||
}
|
||||
|
||||
impl Stream for Body {
|
||||
/// A stream of data frames.
|
||||
///
|
||||
/// Created with [`Body::into_data_stream`].
|
||||
#[derive(Debug)]
|
||||
pub struct BodyDataStream {
|
||||
inner: Body,
|
||||
}
|
||||
|
||||
impl Stream for BodyDataStream {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
#[inline]
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
self.poll_data(cx)
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
match futures_util::ready!(Pin::new(&mut self.inner).poll_frame(cx)?) {
|
||||
Some(frame) => match frame.into_data() {
|
||||
Ok(data) => return Poll::Ready(Some(Ok(data))),
|
||||
Err(_frame) => {}
|
||||
},
|
||||
None => return Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl http_body::Body for BodyDataStream {
|
||||
type Data = Bytes;
|
||||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
Pin::new(&mut self.inner).poll_frame(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_end_stream(&self) -> bool {
|
||||
self.inner.is_end_stream()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> http_body::SizeHint {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,25 +197,17 @@ where
|
|||
type Data = Bytes;
|
||||
type Error = Error;
|
||||
|
||||
fn poll_data(
|
||||
fn poll_frame(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
let stream = self.project().stream.get_pin_mut();
|
||||
match futures_util::ready!(stream.try_poll_next(cx)) {
|
||||
Some(Ok(chunk)) => Poll::Ready(Some(Ok(chunk.into()))),
|
||||
Some(Ok(chunk)) => Poll::Ready(Some(Ok(Frame::data(chunk.into())))),
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(Error::new(err)))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_trailers(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
|
||||
Poll::Ready(Ok(None))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::body::Body;
|
||||
use crate::extract::{DefaultBodyLimitKind, FromRequest, FromRequestParts, Request};
|
||||
use futures_util::future::BoxFuture;
|
||||
use http_body::Limited;
|
||||
|
||||
mod sealed {
|
||||
pub trait Sealed {}
|
||||
|
@ -258,13 +257,13 @@ pub trait RequestExt: sealed::Sealed + Sized {
|
|||
|
||||
/// Apply the [default body limit](crate::extract::DefaultBodyLimit).
|
||||
///
|
||||
/// If it is disabled, return the request as-is in `Err`.
|
||||
fn with_limited_body(self) -> Result<Request<Limited<Body>>, Request>;
|
||||
/// If it is disabled, the request is returned as-is.
|
||||
fn with_limited_body(self) -> Request;
|
||||
|
||||
/// Consumes the request, returning the body wrapped in [`Limited`] if a
|
||||
/// Consumes the request, returning the body wrapped in [`http_body_util::Limited`] if a
|
||||
/// [default limit](crate::extract::DefaultBodyLimit) is in place, or not wrapped if the
|
||||
/// default limit is disabled.
|
||||
fn into_limited_body(self) -> Result<Limited<Body>, Body>;
|
||||
fn into_limited_body(self) -> Body;
|
||||
}
|
||||
|
||||
impl RequestExt for Request {
|
||||
|
@ -305,7 +304,7 @@ impl RequestExt for Request {
|
|||
*req.uri_mut() = self.uri().clone();
|
||||
*req.headers_mut() = std::mem::take(self.headers_mut());
|
||||
*req.extensions_mut() = std::mem::take(self.extensions_mut());
|
||||
let (mut parts, _) = req.into_parts();
|
||||
let (mut parts, ()) = req.into_parts();
|
||||
|
||||
Box::pin(async move {
|
||||
let result = E::from_request_parts(&mut parts, state).await;
|
||||
|
@ -320,24 +319,22 @@ impl RequestExt for Request {
|
|||
})
|
||||
}
|
||||
|
||||
fn with_limited_body(self) -> Result<Request<Limited<Body>>, Request> {
|
||||
fn with_limited_body(self) -> Request {
|
||||
// update docs in `axum-core/src/extract/default_body_limit.rs` and
|
||||
// `axum/src/docs/extract.md` if this changes
|
||||
const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb
|
||||
|
||||
match self.extensions().get::<DefaultBodyLimitKind>().copied() {
|
||||
Some(DefaultBodyLimitKind::Disable) => Err(self),
|
||||
Some(DefaultBodyLimitKind::Disable) => self,
|
||||
Some(DefaultBodyLimitKind::Limit(limit)) => {
|
||||
Ok(self.map(|b| http_body::Limited::new(b, limit)))
|
||||
self.map(|b| Body::new(http_body_util::Limited::new(b, limit)))
|
||||
}
|
||||
None => Ok(self.map(|b| http_body::Limited::new(b, DEFAULT_LIMIT))),
|
||||
None => self.map(|b| Body::new(http_body_util::Limited::new(b, DEFAULT_LIMIT))),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_limited_body(self) -> Result<Limited<Body>, Body> {
|
||||
self.with_limited_body()
|
||||
.map(Request::into_body)
|
||||
.map_err(Request::into_body)
|
||||
fn into_limited_body(self) -> Body {
|
||||
self.with_limited_body().into_body()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ use tower_layer::Layer;
|
|||
///
|
||||
/// This middleware provides ways to configure that.
|
||||
///
|
||||
/// Note that if an extractor consumes the body directly with [`Body::data`], or similar, the
|
||||
/// Note that if an extractor consumes the body directly with [`Body::poll_frame`], or similar, the
|
||||
/// default limit is _not_ applied.
|
||||
///
|
||||
/// # Difference between `DefaultBodyLimit` and [`RequestBodyLimit`]
|
||||
|
@ -22,8 +22,7 @@ use tower_layer::Layer;
|
|||
/// [`RequestBodyLimit`] is applied globally to all requests, regardless of which extractors are
|
||||
/// used or how the body is consumed.
|
||||
///
|
||||
/// `DefaultBodyLimit` is also easier to integrate into an existing setup since it doesn't change
|
||||
/// the request body type:
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use axum::{
|
||||
|
@ -34,34 +33,15 @@ use tower_layer::Layer;
|
|||
/// };
|
||||
///
|
||||
/// let app = Router::new()
|
||||
/// .route(
|
||||
/// "/",
|
||||
/// // even with `DefaultBodyLimit` the request body is still just `Body`
|
||||
/// post(|request: Request| async {}),
|
||||
/// )
|
||||
/// .route("/", post(|request: Request| async {}))
|
||||
/// // change the default limit
|
||||
/// .layer(DefaultBodyLimit::max(1024));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use axum::{Router, routing::post, body::Body, extract::Request};
|
||||
/// use tower_http::limit::RequestBodyLimitLayer;
|
||||
/// use http_body::Limited;
|
||||
///
|
||||
/// let app = Router::new()
|
||||
/// .route(
|
||||
/// "/",
|
||||
/// // `RequestBodyLimitLayer` changes the request body type to `Limited<Body>`
|
||||
/// // extracting a different body type wont work
|
||||
/// post(|request: Request| async {}),
|
||||
/// )
|
||||
/// .layer(RequestBodyLimitLayer::new(1024));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// In general using `DefaultBodyLimit` is recommended but if you need to use third party
|
||||
/// extractors and want to sure a limit is also applied there then [`RequestBodyLimit`] should be
|
||||
/// used.
|
||||
/// extractors and want to make sure a limit is also applied there then [`RequestBodyLimit`] should
|
||||
/// be used.
|
||||
///
|
||||
/// # Different limits for different routes
|
||||
///
|
||||
|
@ -84,10 +64,10 @@ use tower_layer::Layer;
|
|||
/// # let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// [`Body::data`]: http_body::Body::data
|
||||
/// [`Body::poll_frame`]: http_body::Body::poll_frame
|
||||
/// [`Bytes`]: bytes::Bytes
|
||||
/// [`Json`]: https://docs.rs/axum/0.6.0/axum/struct.Json.html
|
||||
/// [`Form`]: https://docs.rs/axum/0.6.0/axum/struct.Form.html
|
||||
/// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html
|
||||
/// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.html
|
||||
/// [`FromRequest`]: crate::extract::FromRequest
|
||||
/// [`RequestBodyLimit`]: tower_http::limit::RequestBodyLimit
|
||||
/// [`RequestExt::with_limited_body`]: crate::RequestExt::with_limited_body
|
||||
|
@ -123,7 +103,7 @@ impl DefaultBodyLimit {
|
|||
/// extract::DefaultBodyLimit,
|
||||
/// };
|
||||
/// use tower_http::limit::RequestBodyLimitLayer;
|
||||
/// use http_body::Limited;
|
||||
/// use http_body_util::Limited;
|
||||
///
|
||||
/// let app: Router<()> = Router::new()
|
||||
/// .route("/", get(|body: Bytes| async {}))
|
||||
|
@ -134,8 +114,8 @@ impl DefaultBodyLimit {
|
|||
/// ```
|
||||
///
|
||||
/// [`Bytes`]: bytes::Bytes
|
||||
/// [`Json`]: https://docs.rs/axum/0.6.0/axum/struct.Json.html
|
||||
/// [`Form`]: https://docs.rs/axum/0.6.0/axum/struct.Form.html
|
||||
/// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html
|
||||
/// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.html
|
||||
pub fn disable() -> Self {
|
||||
Self {
|
||||
kind: DefaultBodyLimitKind::Disable,
|
||||
|
@ -158,7 +138,7 @@ impl DefaultBodyLimit {
|
|||
/// extract::DefaultBodyLimit,
|
||||
/// };
|
||||
/// use tower_http::limit::RequestBodyLimitLayer;
|
||||
/// use http_body::Limited;
|
||||
/// use http_body_util::Limited;
|
||||
///
|
||||
/// let app: Router<()> = Router::new()
|
||||
/// .route("/", get(|body: Bytes| async {}))
|
||||
|
@ -167,8 +147,8 @@ impl DefaultBodyLimit {
|
|||
/// ```
|
||||
///
|
||||
/// [`Bytes::from_request`]: bytes::Bytes
|
||||
/// [`Json`]: https://docs.rs/axum/0.6.0/axum/struct.Json.html
|
||||
/// [`Form`]: https://docs.rs/axum/0.6.0/axum/struct.Form.html
|
||||
/// [`Json`]: https://docs.rs/axum/0.7/axum/struct.Json.html
|
||||
/// [`Form`]: https://docs.rs/axum/0.7/axum/struct.Form.html
|
||||
pub fn max(limit: usize) -> Self {
|
||||
Self {
|
||||
kind: DefaultBodyLimitKind::Limit(limit),
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
///
|
||||
/// This trait can be derived using `#[derive(FromRef)]`.
|
||||
///
|
||||
/// [`State`]: https://docs.rs/axum/0.6/axum/extract/struct.State.html
|
||||
/// [`State`]: https://docs.rs/axum/0.7/axum/extract/struct.State.html
|
||||
// NOTE: This trait is defined in axum-core, even though it is mainly used with `State` which is
|
||||
// defined in axum. That allows crate authors to use it when implementing extractors.
|
||||
pub trait FromRef<T> {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! See [`axum::extract`] for more details.
|
||||
//!
|
||||
//! [`axum::extract`]: https://docs.rs/axum/latest/axum/extract/index.html
|
||||
//! [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
|
||||
|
||||
use crate::{body::Body, response::IntoResponse};
|
||||
use async_trait::async_trait;
|
||||
|
@ -41,12 +41,12 @@ mod private {
|
|||
///
|
||||
/// See [`axum::extract`] for more general docs about extractors.
|
||||
///
|
||||
/// [`axum::extract`]: https://docs.rs/axum/0.6.0/axum/extract/index.html
|
||||
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
|
||||
#[async_trait]
|
||||
#[cfg_attr(
|
||||
nightly_error_messages,
|
||||
rustc_on_unimplemented(
|
||||
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/latest/axum/extract/index.html` for details",
|
||||
#[rustversion::attr(
|
||||
since(1.78),
|
||||
diagnostic::on_unimplemented(
|
||||
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/0.7/axum/extract/index.html` for details",
|
||||
)
|
||||
)]
|
||||
pub trait FromRequestParts<S>: Sized {
|
||||
|
@ -68,12 +68,12 @@ pub trait FromRequestParts<S>: Sized {
|
|||
///
|
||||
/// See [`axum::extract`] for more general docs about extractors.
|
||||
///
|
||||
/// [`axum::extract`]: https://docs.rs/axum/0.6.0/axum/extract/index.html
|
||||
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
|
||||
#[async_trait]
|
||||
#[cfg_attr(
|
||||
nightly_error_messages,
|
||||
rustc_on_unimplemented(
|
||||
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/latest/axum/extract/index.html` for details",
|
||||
#[rustversion::attr(
|
||||
since(1.78),
|
||||
diagnostic::on_unimplemented(
|
||||
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/0.7/axum/extract/index.html` for details",
|
||||
)
|
||||
)]
|
||||
pub trait FromRequest<S, M = private::ViaRequest>: Sized {
|
||||
|
|
|
@ -19,11 +19,18 @@ impl FailedToBufferBody {
|
|||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
// two layers of boxes here because `with_limited_body`
|
||||
// wraps the `http_body_util::Limited` in a `axum_core::Body`
|
||||
// which also wraps the error type
|
||||
let box_error = match err.into().downcast::<Error>() {
|
||||
Ok(err) => err.into_inner(),
|
||||
Err(err) => err,
|
||||
};
|
||||
match box_error.downcast::<http_body::LengthLimitError>() {
|
||||
let box_error = match box_error.downcast::<Error>() {
|
||||
Ok(err) => err.into_inner(),
|
||||
Err(err) => err,
|
||||
};
|
||||
match box_error.downcast::<http_body_util::LengthLimitError>() {
|
||||
Ok(err) => Self::LengthLimitError(LengthLimitError::from_err(err)),
|
||||
Err(err) => Self::UnknownBodyError(UnknownBodyError::from_err(err)),
|
||||
}
|
||||
|
@ -36,7 +43,7 @@ define_rejection! {
|
|||
/// Encountered some other error when buffering the body.
|
||||
///
|
||||
/// This can _only_ happen when you're using [`tower_http::limit::RequestBodyLimitLayer`] or
|
||||
/// otherwise wrapping request bodies in [`http_body::Limited`].
|
||||
/// otherwise wrapping request bodies in [`http_body_util::Limited`].
|
||||
pub struct LengthLimitError(Error);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@ use super::{rejection::*, FromRequest, FromRequestParts, Request};
|
|||
use crate::{body::Body, RequestExt};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use http::{request::Parts, HeaderMap, Method, Uri, Version};
|
||||
use http::{request::Parts, Extensions, HeaderMap, Method, Uri, Version};
|
||||
use http_body_util::BodyExt;
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[async_trait]
|
||||
|
@ -57,7 +58,7 @@ where
|
|||
///
|
||||
/// Prefer using [`TypedHeader`] to extract only the headers you need.
|
||||
///
|
||||
/// [`TypedHeader`]: https://docs.rs/axum/latest/axum/extract/struct.TypedHeader.html
|
||||
/// [`TypedHeader`]: https://docs.rs/axum/0.7/axum/extract/struct.TypedHeader.html
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for HeaderMap
|
||||
where
|
||||
|
@ -78,14 +79,12 @@ where
|
|||
type Rejection = BytesRejection;
|
||||
|
||||
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
|
||||
let bytes = match req.into_limited_body() {
|
||||
Ok(limited_body) => crate::body::to_bytes(limited_body)
|
||||
.await
|
||||
.map_err(FailedToBufferBody::from_err)?,
|
||||
Err(unlimited_body) => crate::body::to_bytes(unlimited_body)
|
||||
.await
|
||||
.map_err(FailedToBufferBody::from_err)?,
|
||||
};
|
||||
let bytes = req
|
||||
.into_limited_body()
|
||||
.collect()
|
||||
.await
|
||||
.map_err(FailedToBufferBody::from_err)?
|
||||
.to_bytes();
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
@ -116,14 +115,26 @@ where
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequest<S> for Parts
|
||||
impl<S> FromRequestParts<S> for Parts
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
|
||||
Ok(req.into_parts().0)
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Ok(parts.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Extensions
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Ok(parts.extensions.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#![cfg_attr(nightly_error_messages, allow(internal_features), feature(rustc_attrs))]
|
||||
//! Core types and traits for [`axum`].
|
||||
//!
|
||||
//! Libraries authors that want to provide [`FromRequest`] or [`IntoResponse`] implementations
|
||||
|
@ -44,7 +43,7 @@
|
|||
missing_debug_implementations,
|
||||
missing_docs
|
||||
)]
|
||||
#![deny(unreachable_pub, private_in_public)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![allow(elided_lifetimes_in_paths, clippy::type_complexity)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(test, allow(clippy::float_cmp))]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/// Private API.
|
||||
#[cfg(feature = "tracing")]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __log_rejection {
|
||||
|
@ -7,7 +8,6 @@ macro_rules! __log_rejection {
|
|||
body_text = $body_text:expr,
|
||||
status = $status:expr,
|
||||
) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
tracing::event!(
|
||||
target: "axum::rejection",
|
||||
|
@ -21,6 +21,17 @@ macro_rules! __log_rejection {
|
|||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __log_rejection {
|
||||
(
|
||||
rejection_type = $ty:ident,
|
||||
body_text = $body_text:expr,
|
||||
status = $status:expr,
|
||||
) => {};
|
||||
}
|
||||
|
||||
/// Private API.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
|
@ -193,7 +204,7 @@ macro_rules! __composite_rejection {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
$(
|
||||
Self::$variant(inner) => write!(f, "{}", inner),
|
||||
Self::$variant(inner) => write!(f, "{inner}"),
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
@ -211,46 +222,6 @@ macro_rules! __composite_rejection {
|
|||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod composite_rejection_tests {
|
||||
use self::defs::*;
|
||||
use crate::Error;
|
||||
use std::error::Error as _;
|
||||
|
||||
#[allow(dead_code, unreachable_pub)]
|
||||
mod defs {
|
||||
use crate::{__composite_rejection, __define_rejection};
|
||||
|
||||
__define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "error message 1"]
|
||||
pub struct Inner1;
|
||||
}
|
||||
__define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "error message 2"]
|
||||
pub struct Inner2(Error);
|
||||
}
|
||||
__composite_rejection! {
|
||||
pub enum Outer { Inner1, Inner2 }
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of `.source()` on `Outer` should defer straight to the implementation
|
||||
/// on its inner type instead of returning the inner type itself, because the `Display`
|
||||
/// implementation on `Outer` already forwards to the inner type and so it would result in two
|
||||
/// errors in the chain `Display`ing the same thing.
|
||||
#[test]
|
||||
fn source_gives_inner_source() {
|
||||
let rejection = Outer::Inner1(Inner1);
|
||||
assert!(rejection.source().is_none());
|
||||
|
||||
let msg = "hello world";
|
||||
let rejection = Outer::Inner2(Inner2(Error::new(msg)));
|
||||
assert_eq!(rejection.source().unwrap().to_string(), msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
macro_rules! all_the_tuples {
|
||||
($name:ident) => {
|
||||
|
@ -334,3 +305,41 @@ macro_rules! __impl_deref {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod composite_rejection_tests {
|
||||
use self::defs::*;
|
||||
use crate::Error;
|
||||
use std::error::Error as _;
|
||||
|
||||
#[allow(dead_code, unreachable_pub)]
|
||||
mod defs {
|
||||
__define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "error message 1"]
|
||||
pub struct Inner1;
|
||||
}
|
||||
__define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "error message 2"]
|
||||
pub struct Inner2(Error);
|
||||
}
|
||||
__composite_rejection! {
|
||||
pub enum Outer { Inner1, Inner2 }
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of `.source()` on `Outer` should defer straight to the implementation
|
||||
/// on its inner type instead of returning the inner type itself, because the `Display`
|
||||
/// implementation on `Outer` already forwards to the inner type and so it would result in two
|
||||
/// errors in the chain `Display`ing the same thing.
|
||||
#[test]
|
||||
fn source_gives_inner_source() {
|
||||
let rejection = Outer::Inner1(Inner1);
|
||||
assert!(rejection.source().is_none());
|
||||
|
||||
let msg = "hello world";
|
||||
let rejection = Outer::Inner2(Inner2(Error::new(msg)));
|
||||
assert_eq!(rejection.source().unwrap().to_string(), msg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ use std::fmt;
|
|||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[must_use]
|
||||
pub struct AppendHeaders<I>(pub I);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use http::{
|
|||
header::{self, HeaderMap, HeaderName, HeaderValue},
|
||||
Extensions, StatusCode,
|
||||
};
|
||||
use http_body::SizeHint;
|
||||
use http_body::{Frame, SizeHint};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
convert::Infallible,
|
||||
|
@ -47,7 +47,7 @@ use std::{
|
|||
/// MyError::SomethingElseWentWrong => "something else went wrong",
|
||||
/// };
|
||||
///
|
||||
/// // its often easiest to implement `IntoResponse` by calling other implementations
|
||||
/// // it's often easiest to implement `IntoResponse` by calling other implementations
|
||||
/// (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
|
||||
/// }
|
||||
/// }
|
||||
|
@ -58,9 +58,7 @@ use std::{
|
|||
/// async fn handler() -> Result<(), MyError> {
|
||||
/// Err(MyError::SomethingWentWrong)
|
||||
/// }
|
||||
/// # async {
|
||||
/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
/// # };
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// Or if you have a custom body type you'll also need to implement
|
||||
|
@ -76,6 +74,7 @@ use std::{
|
|||
/// };
|
||||
/// use http::HeaderMap;
|
||||
/// use bytes::Bytes;
|
||||
/// use http_body::Frame;
|
||||
/// use std::{
|
||||
/// convert::Infallible,
|
||||
/// task::{Poll, Context},
|
||||
|
@ -90,18 +89,10 @@ use std::{
|
|||
/// type Data = Bytes;
|
||||
/// type Error = Infallible;
|
||||
///
|
||||
/// fn poll_data(
|
||||
/// fn poll_frame(
|
||||
/// self: Pin<&mut Self>,
|
||||
/// cx: &mut Context<'_>
|
||||
/// ) -> Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||
/// # unimplemented!()
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn poll_trailers(
|
||||
/// self: Pin<&mut Self>,
|
||||
/// cx: &mut Context<'_>
|
||||
/// ) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
|
||||
/// cx: &mut Context<'_>,
|
||||
/// ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
/// # unimplemented!()
|
||||
/// // ...
|
||||
/// }
|
||||
|
@ -256,30 +247,23 @@ where
|
|||
type Data = Bytes;
|
||||
type Error = Infallible;
|
||||
|
||||
fn poll_data(
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
if let Some(mut buf) = self.first.take() {
|
||||
let bytes = buf.copy_to_bytes(buf.remaining());
|
||||
return Poll::Ready(Some(Ok(bytes)));
|
||||
return Poll::Ready(Some(Ok(Frame::data(bytes))));
|
||||
}
|
||||
|
||||
if let Some(mut buf) = self.second.take() {
|
||||
let bytes = buf.copy_to_bytes(buf.remaining());
|
||||
return Poll::Ready(Some(Ok(bytes)));
|
||||
return Poll::Ready(Some(Ok(Frame::data(bytes))));
|
||||
}
|
||||
|
||||
Poll::Ready(None)
|
||||
}
|
||||
|
||||
fn poll_trailers(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
|
||||
Poll::Ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_end_stream(&self) -> bool {
|
||||
self.first.is_none() && self.second.is_none()
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ use std::{convert::Infallible, fmt};
|
|||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Its also recommended to implement `IntoResponse` so `SetHeader` can be used on its own as
|
||||
/// // It's also recommended to implement `IntoResponse` so `SetHeader` can be used on its own as
|
||||
/// // the response
|
||||
/// impl<'a> IntoResponse for SetHeader<'a> {
|
||||
/// fn into_response(self) -> Response {
|
||||
|
@ -258,3 +258,11 @@ impl IntoResponseParts for Extensions {
|
|||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponseParts for () {
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! See [`axum::response`] for more details.
|
||||
//!
|
||||
//! [`axum::response`]: https://docs.rs/axum/latest/axum/response/index.html
|
||||
//! [`axum::response`]: https://docs.rs/axum/0.7/axum/response/index.html
|
||||
|
||||
use crate::body::Body;
|
||||
|
||||
|
|
|
@ -7,13 +7,71 @@ and this project adheres to [Semantic Versioning].
|
|||
|
||||
# Unreleased
|
||||
|
||||
- None.
|
||||
|
||||
# 0.9.3 (24. March, 2024)
|
||||
|
||||
- **added:** New `tracing` feature which enables logging rejections from
|
||||
built-in extractor with the `axum::rejection=trace` target ([#2584])
|
||||
|
||||
[#2584]: https://github.com/tokio-rs/axum/pull/2584
|
||||
|
||||
# 0.9.2 (13. January, 2024)
|
||||
|
||||
- **added:** Implement `TypedPath` for `WithRejection<TypedPath, _>`
|
||||
- **fixed:** Documentation link to `serde::Deserialize` in `JsonDeserializer` extractor ([#2498])
|
||||
- **added:** Add `is_missing` function for `TypedHeaderRejection` and `TypedHeaderRejectionReason` ([#2503])
|
||||
|
||||
[#2498]: https://github.com/tokio-rs/axum/pull/2498
|
||||
[#2503]: https://github.com/tokio-rs/axum/pull/2503
|
||||
|
||||
# 0.9.1 (29. December, 2023)
|
||||
|
||||
- **change:** Update version of multer used internally for multipart ([#2433])
|
||||
- **added:** `JsonDeserializer` extractor ([#2431])
|
||||
|
||||
[#2433]: https://github.com/tokio-rs/axum/pull/2433
|
||||
[#2431]: https://github.com/tokio-rs/axum/pull/2431
|
||||
|
||||
# 0.9.0 (27. November, 2023)
|
||||
|
||||
- **added:** `OptionalQuery` extractor ([#2310])
|
||||
- **added:** `TypedHeader` which used to be in `axum` ([#1850])
|
||||
- **added:** `Clone` implementation for `ErasedJson` ([#2142])
|
||||
- **breaking:** Update to prost 0.12. Used for the `Protobuf` extractor
|
||||
- **breaking:** Make `tokio` an optional dependency
|
||||
- **breaking:** Upgrade `cookie` dependency to 0.18 ([#2343])
|
||||
- **breaking:** Functions and methods that previously accepted a `Cookie`
|
||||
now accept any `T: Into<Cookie>` ([#2348])
|
||||
|
||||
[#1850]: https://github.com/tokio-rs/axum/pull/1850
|
||||
[#2310]: https://github.com/tokio-rs/axum/pull/2310
|
||||
[#2343]: https://github.com/tokio-rs/axum/pull/2343
|
||||
[#2348]: https://github.com/tokio-rs/axum/pull/2348
|
||||
|
||||
# 0.8.0 (16. September, 2023)
|
||||
|
||||
- **breaking:** Update to prost 0.12. Used for the `Protobuf` extractor ([#2224])
|
||||
|
||||
[#2224]: https://github.com/tokio-rs/axum/pull/2224
|
||||
|
||||
# 0.7.7 (03. August, 2023)
|
||||
|
||||
- **added:** `Clone` implementation for `ErasedJson` ([#2142])
|
||||
|
||||
[#2142]: https://github.com/tokio-rs/axum/pull/2142
|
||||
|
||||
# 0.7.6 (02. August, 2023)
|
||||
|
||||
- **fixed:** Remove unused dependency ([#2135])
|
||||
|
||||
[#2135]: https://github.com/tokio-rs/axum/pull/2135
|
||||
|
||||
# 0.7.5 (17. July, 2023)
|
||||
|
||||
- **fixed:** Remove explicit auto deref from `PrivateCookieJar` example ([#2028])
|
||||
|
||||
[#2028]: https://github.com/tokio-rs/axum/pull/2028
|
||||
|
||||
# 0.7.4 (18. April, 2023)
|
||||
|
||||
- **added:** Add `Html` response type ([#1921])
|
||||
|
|
|
@ -2,77 +2,81 @@
|
|||
categories = ["asynchronous", "network-programming", "web-programming"]
|
||||
description = "Extra utilities for axum"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
rust-version = "1.66"
|
||||
homepage = "https://github.com/tokio-rs/axum"
|
||||
keywords = ["http", "web", "framework"]
|
||||
license = "MIT"
|
||||
name = "axum-extra"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tokio-rs/axum"
|
||||
version = "0.7.4"
|
||||
version = "0.9.3"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["tracing"]
|
||||
|
||||
async-read-body = ["dep:tokio-util", "tokio-util?/io"]
|
||||
async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"]
|
||||
cookie = ["dep:cookie"]
|
||||
cookie-private = ["cookie", "cookie?/private"]
|
||||
cookie-signed = ["cookie", "cookie?/signed"]
|
||||
cookie-key-expansion = ["cookie", "cookie?/key-expansion"]
|
||||
erased-json = ["dep:serde_json"]
|
||||
form = ["dep:serde_html_form"]
|
||||
json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"]
|
||||
json-lines = [
|
||||
"dep:serde_json",
|
||||
"dep:tokio-util",
|
||||
"dep:tokio-stream",
|
||||
"tokio-util?/io",
|
||||
"tokio-stream?/io-util"
|
||||
"tokio-stream?/io-util",
|
||||
"dep:tokio",
|
||||
]
|
||||
multipart = ["dep:multer"]
|
||||
protobuf = ["dep:prost"]
|
||||
query = ["dep:serde_html_form"]
|
||||
tracing = ["dep:tracing", "axum-core/tracing", "axum/tracing"]
|
||||
typed-header = ["dep:headers"]
|
||||
typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"]
|
||||
|
||||
[dependencies]
|
||||
axum = { path = "../axum", version = "0.6.13", default-features = false }
|
||||
axum-core = { path = "../axum-core", version = "0.3.4" }
|
||||
axum = { path = "../axum", version = "0.7.2", default-features = false }
|
||||
axum-core = { path = "../axum-core", version = "0.4.3" }
|
||||
bytes = "1.1.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||
http = "0.2"
|
||||
http-body = "0.4.4"
|
||||
http = "1.0.0"
|
||||
http-body = "1.0.0"
|
||||
http-body-util = "0.1.0"
|
||||
mime = "0.3"
|
||||
pin-project-lite = "0.2"
|
||||
serde = "1.0"
|
||||
tokio = "1.19"
|
||||
tower = { version = "0.4", default_features = false, features = ["util"] }
|
||||
tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
|
||||
# optional dependencies
|
||||
axum-macros = { path = "../axum-macros", version = "0.3.7", optional = true }
|
||||
cookie = { package = "cookie", version = "0.17", features = ["percent-encode"], optional = true }
|
||||
axum-macros = { path = "../axum-macros", version = "0.4.1", optional = true }
|
||||
cookie = { package = "cookie", version = "0.18.0", features = ["percent-encode"], optional = true }
|
||||
form_urlencoded = { version = "1.1.0", optional = true }
|
||||
headers = { version = "0.3.8", optional = true }
|
||||
multer = { version = "2.0.0", optional = true }
|
||||
headers = { version = "0.4.0", optional = true }
|
||||
multer = { version = "3.0.0", optional = true }
|
||||
percent-encoding = { version = "2.1", optional = true }
|
||||
prost = { version = "0.12", optional = true }
|
||||
serde_html_form = { version = "0.2.0", optional = true }
|
||||
serde_json = { version = "1.0.71", optional = true }
|
||||
serde_path_to_error = { version = "0.1.8", optional = true }
|
||||
tokio = { version = "1.19", optional = true }
|
||||
tokio-stream = { version = "0.1.9", optional = true }
|
||||
tokio-util = { version = "0.7", optional = true }
|
||||
tracing = { version = "0.1.37", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { path = "../axum", version = "0.6.0" }
|
||||
futures = "0.3"
|
||||
http-body = "0.4.4"
|
||||
hyper = "0.14"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "multipart"] }
|
||||
axum = { path = "../axum", version = "0.7.2" }
|
||||
hyper = "1.0.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.71"
|
||||
tokio = { version = "1.14", features = ["full"] }
|
||||
tower = { version = "0.4", features = ["util"] }
|
||||
tower-http = { version = "0.4", features = ["map-response-body", "timeout"] }
|
||||
tower-http = { version = "0.5.0", features = ["map-response-body", "timeout"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
|
|
@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
|
|||
|
||||
## Minimum supported Rust version
|
||||
|
||||
axum-extra's MSRV is 1.63.
|
||||
axum-extra's MSRV is 1.66.
|
||||
|
||||
## Getting Help
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use axum::{
|
||||
body::{Body, Bytes, HttpBody},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
Error,
|
||||
};
|
||||
|
@ -33,7 +32,7 @@ pin_project! {
|
|||
/// let file = File::open("Cargo.toml")
|
||||
/// .await
|
||||
/// .map_err(|err| {
|
||||
/// (StatusCode::NOT_FOUND, format!("File not found: {}", err))
|
||||
/// (StatusCode::NOT_FOUND, format!("File not found: {err}"))
|
||||
/// })?;
|
||||
///
|
||||
/// let headers = [(CONTENT_TYPE, "text/x-toml")];
|
||||
|
@ -69,18 +68,22 @@ impl HttpBody for AsyncReadBody {
|
|||
type Data = Bytes;
|
||||
type Error = Error;
|
||||
|
||||
fn poll_data(
|
||||
#[inline]
|
||||
fn poll_frame(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||
self.project().body.poll_data(cx)
|
||||
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
|
||||
self.project().body.poll_frame(cx)
|
||||
}
|
||||
|
||||
fn poll_trailers(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
|
||||
self.project().body.poll_trailers(cx)
|
||||
#[inline]
|
||||
fn is_end_stream(&self) -> bool {
|
||||
self.body.is_end_stream()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> http_body::SizeHint {
|
||||
self.body.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -111,8 +111,7 @@ axum_core::__impl_deref!(Cached);
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{extract::FromRequestParts, http::Request, routing::get, Router};
|
||||
use http::request::Parts;
|
||||
use axum::{http::Request, routing::get, Router};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
|
|
|
@ -168,11 +168,11 @@ impl CookieJar {
|
|||
/// use axum::response::IntoResponse;
|
||||
///
|
||||
/// async fn handle(jar: CookieJar) -> CookieJar {
|
||||
/// jar.remove(Cookie::named("foo"))
|
||||
/// jar.remove(Cookie::from("foo"))
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn remove(mut self, cookie: Cookie<'static>) -> Self {
|
||||
pub fn remove<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
|
||||
self.jar.remove(cookie);
|
||||
self
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ impl CookieJar {
|
|||
/// ```
|
||||
#[must_use]
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn add(mut self, cookie: Cookie<'static>) -> Self {
|
||||
pub fn add<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
|
||||
self.jar.add(cookie);
|
||||
self
|
||||
}
|
||||
|
@ -234,6 +234,7 @@ fn set_cookies(jar: cookie::CookieJar, headers: &mut HeaderMap) {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use axum::{body::Body, extract::FromRef, http::Request, routing::get, Router};
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
macro_rules! cookie_test {
|
||||
|
@ -249,7 +250,7 @@ mod tests {
|
|||
}
|
||||
|
||||
async fn remove_cookie(jar: $jar) -> impl IntoResponse {
|
||||
jar.remove(Cookie::named("key"))
|
||||
jar.remove(Cookie::from("key"))
|
||||
}
|
||||
|
||||
let state = AppState {
|
||||
|
@ -378,7 +379,7 @@ mod tests {
|
|||
B: axum::body::HttpBody,
|
||||
B::Error: std::fmt::Debug,
|
||||
{
|
||||
let bytes = hyper::body::to_bytes(body).await.unwrap();
|
||||
let bytes = body.collect().await.unwrap().to_bytes();
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,11 +216,11 @@ impl<K> PrivateCookieJar<K> {
|
|||
/// use axum::response::IntoResponse;
|
||||
///
|
||||
/// async fn handle(jar: PrivateCookieJar) -> PrivateCookieJar {
|
||||
/// jar.remove(Cookie::named("foo"))
|
||||
/// jar.remove(Cookie::from("foo"))
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn remove(mut self, cookie: Cookie<'static>) -> Self {
|
||||
pub fn remove<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
|
||||
self.private_jar_mut().remove(cookie);
|
||||
self
|
||||
}
|
||||
|
@ -241,7 +241,7 @@ impl<K> PrivateCookieJar<K> {
|
|||
/// ```
|
||||
#[must_use]
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn add(mut self, cookie: Cookie<'static>) -> Self {
|
||||
pub fn add<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
|
||||
self.private_jar_mut().add(cookie);
|
||||
self
|
||||
}
|
||||
|
|
|
@ -234,11 +234,11 @@ impl<K> SignedCookieJar<K> {
|
|||
/// use axum::response::IntoResponse;
|
||||
///
|
||||
/// async fn handle(jar: SignedCookieJar) -> SignedCookieJar {
|
||||
/// jar.remove(Cookie::named("foo"))
|
||||
/// jar.remove(Cookie::from("foo"))
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn remove(mut self, cookie: Cookie<'static>) -> Self {
|
||||
pub fn remove<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
|
||||
self.signed_jar_mut().remove(cookie);
|
||||
self
|
||||
}
|
||||
|
@ -259,7 +259,7 @@ impl<K> SignedCookieJar<K> {
|
|||
/// ```
|
||||
#[must_use]
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn add(mut self, cookie: Cookie<'static>) -> Self {
|
||||
pub fn add<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
|
||||
self.signed_jar_mut().add(cookie);
|
||||
self
|
||||
}
|
||||
|
|
|
@ -92,11 +92,16 @@ impl IntoResponse for FormRejection {
|
|||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
Self::RawFormRejection(inner) => inner.into_response(),
|
||||
Self::FailedToDeserializeForm(inner) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to deserialize form: {}", inner),
|
||||
)
|
||||
.into_response(),
|
||||
Self::FailedToDeserializeForm(inner) => {
|
||||
let body = format!("Failed to deserialize form: {inner}");
|
||||
let status = StatusCode::BAD_REQUEST;
|
||||
axum_core::__log_rejection!(
|
||||
rejection_type = Self,
|
||||
body_text = body,
|
||||
status = status,
|
||||
);
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +129,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{routing::post, Router};
|
||||
use http::{header::CONTENT_TYPE, StatusCode};
|
||||
use http::header::CONTENT_TYPE;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -146,7 +151,6 @@ mod tests {
|
|||
.post("/")
|
||||
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body("value=one&value=two")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
|
431
axum-extra/src/extract/json_deserializer.rs
Normal file
431
axum-extra/src/extract/json_deserializer.rs
Normal file
|
@ -0,0 +1,431 @@
|
|||
use axum::async_trait;
|
||||
use axum::extract::{FromRequest, Request};
|
||||
use axum_core::__composite_rejection as composite_rejection;
|
||||
use axum_core::__define_rejection as define_rejection;
|
||||
use axum_core::extract::rejection::BytesRejection;
|
||||
use bytes::Bytes;
|
||||
use http::{header, HeaderMap};
|
||||
use serde::Deserialize;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// JSON Extractor for zero-copy deserialization.
|
||||
///
|
||||
/// Deserialize request bodies into some type that implements [`serde::Deserialize<'de>`][serde::Deserialize].
|
||||
/// Parsing JSON is delayed until [`deserialize`](JsonDeserializer::deserialize) is called.
|
||||
/// If the type implements [`serde::de::DeserializeOwned`], the [`Json`](axum::Json) extractor should
|
||||
/// be preferred.
|
||||
///
|
||||
/// The request will be rejected (and a [`JsonDeserializerRejection`] will be returned) if:
|
||||
///
|
||||
/// - The request doesn't have a `Content-Type: application/json` (or similar) header.
|
||||
/// - Buffering the request body fails.
|
||||
///
|
||||
/// Additionally, a `JsonRejection` error will be returned, when calling `deserialize` if:
|
||||
///
|
||||
/// - The body doesn't contain syntactically valid JSON.
|
||||
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target
|
||||
/// type.
|
||||
/// - Attempting to deserialize escaped JSON into a type that must be borrowed (e.g. `&'a str`).
|
||||
///
|
||||
/// ⚠️ `serde` will implicitly try to borrow for `&str` and `&[u8]` types, but will error if the
|
||||
/// input contains escaped characters. Use `Cow<'a, str>` or `Cow<'a, [u8]>`, with the
|
||||
/// `#[serde(borrow)]` attribute, to allow serde to fall back to an owned type when encountering
|
||||
/// escaped characters.
|
||||
///
|
||||
/// ⚠️ Since parsing JSON requires consuming the request body, the `Json` extractor must be
|
||||
/// *last* if there are multiple extractors in a handler.
|
||||
/// See ["the order of extractors"][order-of-extractors]
|
||||
///
|
||||
/// [order-of-extractors]: axum::extract#the-order-of-extractors
|
||||
///
|
||||
/// See [`JsonDeserializerRejection`] for more details.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::{
|
||||
/// routing::post,
|
||||
/// Router,
|
||||
/// response::{IntoResponse, Response}
|
||||
/// };
|
||||
/// use axum_extra::extract::JsonDeserializer;
|
||||
/// use serde::Deserialize;
|
||||
/// use std::borrow::Cow;
|
||||
/// use http::StatusCode;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Data<'a> {
|
||||
/// #[serde(borrow)]
|
||||
/// borrow_text: Cow<'a, str>,
|
||||
/// #[serde(borrow)]
|
||||
/// borrow_bytes: Cow<'a, [u8]>,
|
||||
/// borrow_dangerous: &'a str,
|
||||
/// not_borrowed: String,
|
||||
/// }
|
||||
///
|
||||
/// async fn upload(deserializer: JsonDeserializer<Data<'_>>) -> Response {
|
||||
/// let data = match deserializer.deserialize() {
|
||||
/// Ok(data) => data,
|
||||
/// Err(e) => return e.into_response(),
|
||||
/// };
|
||||
///
|
||||
/// // payload is a `Data` with borrowed data from `deserializer`,
|
||||
/// // which owns the request body (`Bytes`).
|
||||
///
|
||||
/// StatusCode::OK.into_response()
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/upload", post(upload));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
|
||||
pub struct JsonDeserializer<T> {
|
||||
bytes: Bytes,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T, S> FromRequest<S> for JsonDeserializer<T>
|
||||
where
|
||||
T: Deserialize<'static>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = JsonDeserializerRejection;
|
||||
|
||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
if json_content_type(req.headers()) {
|
||||
let bytes = Bytes::from_request(req, state).await?;
|
||||
Ok(Self {
|
||||
bytes,
|
||||
_marker: PhantomData,
|
||||
})
|
||||
} else {
|
||||
Err(MissingJsonContentType.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, 'a: 'de, T> JsonDeserializer<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
/// Deserialize the request body into the target type.
|
||||
/// See [`JsonDeserializer`] for more details.
|
||||
pub fn deserialize(&'a self) -> Result<T, JsonDeserializerRejection> {
|
||||
let deserializer = &mut serde_json::Deserializer::from_slice(&self.bytes);
|
||||
|
||||
let value = match serde_path_to_error::deserialize(deserializer) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let rejection = match err.inner().classify() {
|
||||
serde_json::error::Category::Data => JsonDataError::from_err(err).into(),
|
||||
serde_json::error::Category::Syntax | serde_json::error::Category::Eof => {
|
||||
JsonSyntaxError::from_err(err).into()
|
||||
}
|
||||
serde_json::error::Category::Io => {
|
||||
if cfg!(debug_assertions) {
|
||||
// we don't use `serde_json::from_reader` and instead always buffer
|
||||
// bodies first, so we shouldn't encounter any IO errors
|
||||
unreachable!()
|
||||
} else {
|
||||
JsonSyntaxError::from_err(err).into()
|
||||
}
|
||||
}
|
||||
};
|
||||
return Err(rejection);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
define_rejection! {
|
||||
#[status = UNPROCESSABLE_ENTITY]
|
||||
#[body = "Failed to deserialize the JSON body into the target type"]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
|
||||
/// Rejection type for [`JsonDeserializer`].
|
||||
///
|
||||
/// This rejection is used if the request body is syntactically valid JSON but couldn't be
|
||||
/// deserialized into the target type.
|
||||
pub struct JsonDataError(Error);
|
||||
}
|
||||
|
||||
define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "Failed to parse the request body as JSON"]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
|
||||
/// Rejection type for [`JsonDeserializer`].
|
||||
///
|
||||
/// This rejection is used if the request body didn't contain syntactically valid JSON.
|
||||
pub struct JsonSyntaxError(Error);
|
||||
}
|
||||
|
||||
define_rejection! {
|
||||
#[status = UNSUPPORTED_MEDIA_TYPE]
|
||||
#[body = "Expected request with `Content-Type: application/json`"]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
|
||||
/// Rejection type for [`JsonDeserializer`] used if the `Content-Type`
|
||||
/// header is missing.
|
||||
pub struct MissingJsonContentType;
|
||||
}
|
||||
|
||||
composite_rejection! {
|
||||
/// Rejection used for [`JsonDeserializer`].
|
||||
///
|
||||
/// Contains one variant for each way the [`JsonDeserializer`] extractor
|
||||
/// can fail.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
|
||||
pub enum JsonDeserializerRejection {
|
||||
JsonDataError,
|
||||
JsonSyntaxError,
|
||||
MissingJsonContentType,
|
||||
BytesRejection,
|
||||
}
|
||||
}
|
||||
|
||||
fn json_content_type(headers: &HeaderMap) -> bool {
|
||||
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
||||
content_type
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let content_type = if let Ok(content_type) = content_type.to_str() {
|
||||
content_type
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
|
||||
mime
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let is_json_content_type = mime.type_() == "application"
|
||||
&& (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json"));
|
||||
|
||||
is_json_content_type
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[tokio::test]
|
||||
async fn deserialize_body() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Input<'a> {
|
||||
#[serde(borrow)]
|
||||
foo: Cow<'a, str>,
|
||||
}
|
||||
|
||||
async fn handler(deserializer: JsonDeserializer<Input<'_>>) -> Response {
|
||||
match deserializer.deserialize() {
|
||||
Ok(input) => {
|
||||
assert!(matches!(input.foo, Cow::Borrowed(_)));
|
||||
input.foo.into_owned().into_response()
|
||||
}
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client.post("/").json(&json!({ "foo": "bar" })).await;
|
||||
let body = res.text().await;
|
||||
|
||||
assert_eq!(body, "bar");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deserialize_body_escaped_to_cow() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Input<'a> {
|
||||
#[serde(borrow)]
|
||||
foo: Cow<'a, str>,
|
||||
}
|
||||
|
||||
async fn handler(deserializer: JsonDeserializer<Input<'_>>) -> Response {
|
||||
match deserializer.deserialize() {
|
||||
Ok(Input { foo }) => {
|
||||
let Cow::Owned(foo) = foo else {
|
||||
panic!("Deserializer is expected to fallback to Cow::Owned when encountering escaped characters")
|
||||
};
|
||||
|
||||
foo.into_response()
|
||||
}
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
// The escaped characters prevent serde_json from borrowing.
|
||||
let res = client.post("/").json(&json!({ "foo": "\"bar\"" })).await;
|
||||
|
||||
let body = res.text().await;
|
||||
|
||||
assert_eq!(body, r#""bar""#);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deserialize_body_escaped_to_str() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Input<'a> {
|
||||
// Explicit `#[serde(borrow)]` attribute is not required for `&str` or &[u8].
|
||||
// See: https://serde.rs/lifetimes.html#borrowing-data-in-a-derived-impl
|
||||
foo: &'a str,
|
||||
}
|
||||
|
||||
async fn handler(deserializer: JsonDeserializer<Input<'_>>) -> Response {
|
||||
match deserializer.deserialize() {
|
||||
Ok(Input { foo }) => foo.to_owned().into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.post("/").json(&json!({ "foo": "good" })).await;
|
||||
let body = res.text().await;
|
||||
assert_eq!(body, "good");
|
||||
|
||||
let res = client.post("/").json(&json!({ "foo": "\"bad\"" })).await;
|
||||
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
let body_text = res.text().await;
|
||||
assert_eq!(
|
||||
body_text,
|
||||
"Failed to deserialize the JSON body into the target type: foo: invalid type: string \"\\\"bad\\\"\", expected a borrowed string at line 1 column 16"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consume_body_to_json_requires_json_content_type() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Input<'a> {
|
||||
#[allow(dead_code)]
|
||||
foo: Cow<'a, str>,
|
||||
}
|
||||
|
||||
async fn handler(_deserializer: JsonDeserializer<Input<'_>>) -> Response {
|
||||
panic!("This handler should not be called")
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client.post("/").body(r#"{ "foo": "bar" }"#).await;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn json_content_types() {
|
||||
async fn valid_json_content_type(content_type: &str) -> bool {
|
||||
println!("testing {content_type:?}");
|
||||
|
||||
async fn handler(_deserializer: JsonDeserializer<Value>) -> Response {
|
||||
StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
|
||||
let res = TestClient::new(app)
|
||||
.post("/")
|
||||
.header("content-type", content_type)
|
||||
.body("{}")
|
||||
.await;
|
||||
|
||||
res.status() == StatusCode::OK
|
||||
}
|
||||
|
||||
assert!(valid_json_content_type("application/json").await);
|
||||
assert!(valid_json_content_type("application/json; charset=utf-8").await);
|
||||
assert!(valid_json_content_type("application/json;charset=utf-8").await);
|
||||
assert!(valid_json_content_type("application/cloudevents+json").await);
|
||||
assert!(!valid_json_content_type("text/json").await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_json_syntax() {
|
||||
async fn handler(deserializer: JsonDeserializer<Value>) -> Response {
|
||||
match deserializer.deserialize() {
|
||||
Ok(_) => panic!("Should have matched `Err`"),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client
|
||||
.post("/")
|
||||
.body("{")
|
||||
.header("content-type", "application/json")
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Foo {
|
||||
#[allow(dead_code)]
|
||||
a: i32,
|
||||
#[allow(dead_code)]
|
||||
b: Vec<Bar>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Bar {
|
||||
#[allow(dead_code)]
|
||||
x: i32,
|
||||
#[allow(dead_code)]
|
||||
y: i32,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_json_data() {
|
||||
async fn handler(deserializer: JsonDeserializer<Foo>) -> Response {
|
||||
match deserializer.deserialize() {
|
||||
Ok(_) => panic!("Should have matched `Err`"),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client
|
||||
.post("/")
|
||||
.body("{\"a\": 1, \"b\": [{\"x\": 2}]}")
|
||||
.header("content-type", "application/json")
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
let body_text = res.text().await;
|
||||
assert_eq!(
|
||||
body_text,
|
||||
"Failed to deserialize the JSON body into the target type: b[0]: missing field `y` at line 1 column 23"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,9 @@ mod form;
|
|||
#[cfg(feature = "cookie")]
|
||||
pub mod cookie;
|
||||
|
||||
#[cfg(feature = "json-deserializer")]
|
||||
mod json_deserializer;
|
||||
|
||||
#[cfg(feature = "query")]
|
||||
mod query;
|
||||
|
||||
|
@ -31,11 +34,17 @@ pub use self::cookie::SignedCookieJar;
|
|||
pub use self::form::{Form, FormRejection};
|
||||
|
||||
#[cfg(feature = "query")]
|
||||
pub use self::query::{Query, QueryRejection};
|
||||
pub use self::query::{OptionalQuery, OptionalQueryRejection, Query, QueryRejection};
|
||||
|
||||
#[cfg(feature = "multipart")]
|
||||
pub use self::multipart::Multipart;
|
||||
|
||||
#[cfg(feature = "json-deserializer")]
|
||||
pub use self::json_deserializer::{
|
||||
JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError,
|
||||
MissingJsonContentType,
|
||||
};
|
||||
|
||||
#[cfg(feature = "json-lines")]
|
||||
#[doc(no_inline)]
|
||||
pub use crate::json_lines::JsonLines;
|
||||
|
|
|
@ -99,11 +99,8 @@ where
|
|||
|
||||
async fn from_request(req: Request<Body>, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let boundary = parse_boundary(req.headers()).ok_or(InvalidBoundary)?;
|
||||
let stream = match req.with_limited_body() {
|
||||
Ok(limited) => Body::new(limited),
|
||||
Err(unlimited) => unlimited.into_body(),
|
||||
};
|
||||
let multipart = multer::Multipart::new(stream, boundary);
|
||||
let stream = req.with_limited_body().into_body();
|
||||
let multipart = multer::Multipart::new(stream.into_data_stream(), boundary);
|
||||
Ok(Self { inner: multipart })
|
||||
}
|
||||
}
|
||||
|
@ -283,7 +280,7 @@ fn status_code_from_multer_error(err: &multer::Error) -> StatusCode {
|
|||
if err
|
||||
.downcast_ref::<axum::Error>()
|
||||
.and_then(|err| err.source())
|
||||
.and_then(|err| err.downcast_ref::<http_body::LengthLimitError>())
|
||||
.and_then(|err| err.downcast_ref::<http_body_util::LengthLimitError>())
|
||||
.is_some()
|
||||
{
|
||||
return StatusCode::PAYLOAD_TOO_LARGE;
|
||||
|
@ -382,7 +379,13 @@ pub struct InvalidBoundary;
|
|||
|
||||
impl IntoResponse for InvalidBoundary {
|
||||
fn into_response(self) -> Response {
|
||||
(self.status(), self.body_text()).into_response()
|
||||
let body = self.body_text();
|
||||
axum_core::__log_rejection!(
|
||||
rejection_type = Self,
|
||||
body_text = body,
|
||||
status = self.status(),
|
||||
);
|
||||
(self.status(), body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -410,7 +413,7 @@ impl std::error::Error for InvalidBoundary {}
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{extract::DefaultBodyLimit, response::IntoResponse, routing::post, Router};
|
||||
use axum::{extract::DefaultBodyLimit, routing::post, Router};
|
||||
|
||||
#[tokio::test]
|
||||
async fn content_type_with_encoding() {
|
||||
|
@ -440,7 +443,7 @@ mod tests {
|
|||
.unwrap(),
|
||||
);
|
||||
|
||||
client.post("/").multipart(form).send().await;
|
||||
client.post("/").multipart(form).await;
|
||||
}
|
||||
|
||||
// No need for this to be a #[test], we just want to make sure it compiles
|
||||
|
@ -469,7 +472,7 @@ mod tests {
|
|||
let form =
|
||||
reqwest::multipart::Form::new().part("file", reqwest::multipart::Part::bytes(BYTES));
|
||||
|
||||
let res = client.post("/").multipart(form).send().await;
|
||||
let res = client.post("/").multipart(form).await;
|
||||
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,19 +81,19 @@ mod tests {
|
|||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/").send().await;
|
||||
let res = client.get("/").await;
|
||||
assert_eq!(res.text().await, "Success: 0");
|
||||
|
||||
let res = client.get("/1").send().await;
|
||||
let res = client.get("/1").await;
|
||||
assert_eq!(res.text().await, "Success: 1");
|
||||
|
||||
let res = client.get("/0").send().await;
|
||||
let res = client.get("/0").await;
|
||||
assert_eq!(
|
||||
res.text().await,
|
||||
"Invalid URL: invalid value: integer `0`, expected a nonzero u32"
|
||||
);
|
||||
|
||||
let res = client.get("/NaN").send().await;
|
||||
let res = client.get("/NaN").await;
|
||||
assert_eq!(
|
||||
res.text().await,
|
||||
"Invalid URL: Cannot parse `\"NaN\"` to a `u32`"
|
||||
|
|
|
@ -51,6 +51,33 @@ use std::fmt;
|
|||
/// example.
|
||||
///
|
||||
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
|
||||
///
|
||||
/// While `Option<T>` will handle empty parameters (e.g. `param=`), beware when using this with a
|
||||
/// `Vec<T>`. If your list is optional, use `Vec<T>` in combination with `#[serde(default)]`
|
||||
/// instead of `Option<Vec<T>>`. `Option<Vec<T>>` will handle 0, 2, or more arguments, but not one
|
||||
/// argument.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::{routing::get, Router};
|
||||
/// use axum_extra::extract::Query;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Params {
|
||||
/// #[serde(default)]
|
||||
/// items: Vec<usize>,
|
||||
/// }
|
||||
///
|
||||
/// // This will parse 0 occurrences of `items` as an empty `Vec`.
|
||||
/// async fn process_items(Query(params): Query<Params>) {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/process_items", get(process_items));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Query<T>(pub T);
|
||||
|
@ -97,11 +124,16 @@ impl QueryRejection {
|
|||
impl IntoResponse for QueryRejection {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
Self::FailedToDeserializeQueryString(inner) => (
|
||||
self.status(),
|
||||
format!("Failed to deserialize query string: {}", inner),
|
||||
)
|
||||
.into_response(),
|
||||
Self::FailedToDeserializeQueryString(inner) => {
|
||||
let body = format!("Failed to deserialize query string: {inner}");
|
||||
let status = StatusCode::BAD_REQUEST;
|
||||
axum_core::__log_rejection!(
|
||||
rejection_type = Self,
|
||||
body_text = body,
|
||||
status = status,
|
||||
);
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,16 +154,134 @@ impl std::error::Error for QueryRejection {
|
|||
}
|
||||
}
|
||||
|
||||
/// Extractor that deserializes query strings into `None` if no query parameters are present.
|
||||
/// Otherwise behaviour is identical to [`Query`]
|
||||
///
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::{routing::get, Router};
|
||||
/// use axum_extra::extract::OptionalQuery;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Pagination {
|
||||
/// page: usize,
|
||||
/// per_page: usize,
|
||||
/// }
|
||||
///
|
||||
/// // This will parse query strings like `?page=2&per_page=30` into `Some(Pagination)` and
|
||||
/// // empty query string into `None`
|
||||
/// async fn list_things(OptionalQuery(pagination): OptionalQuery<Pagination>) {
|
||||
/// match pagination {
|
||||
/// Some(Pagination{ page, per_page }) => { /* return specified page */ },
|
||||
/// None => { /* return fist page */ }
|
||||
/// }
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// 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
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct OptionalQuery<T>(pub Option<T>);
|
||||
|
||||
#[async_trait]
|
||||
impl<T, S> FromRequestParts<S> for OptionalQuery<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = OptionalQueryRejection;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
if let Some(query) = parts.uri.query() {
|
||||
let value = serde_html_form::from_str(query).map_err(|err| {
|
||||
OptionalQueryRejection::FailedToDeserializeQueryString(Error::new(err))
|
||||
})?;
|
||||
Ok(OptionalQuery(Some(value)))
|
||||
} else {
|
||||
Ok(OptionalQuery(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for OptionalQuery<T> {
|
||||
type Target = Option<T>;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::DerefMut for OptionalQuery<T> {
|
||||
#[inline]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Rejection used for [`OptionalQuery`].
|
||||
///
|
||||
/// Contains one variant for each way the [`OptionalQuery`] extractor can fail.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "query")]
|
||||
pub enum OptionalQueryRejection {
|
||||
#[allow(missing_docs)]
|
||||
FailedToDeserializeQueryString(Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for OptionalQueryRejection {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
Self::FailedToDeserializeQueryString(inner) => (
|
||||
self.status(),
|
||||
format!("Failed to deserialize query string: {inner}"),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for OptionalQueryRejection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::FailedToDeserializeQueryString(inner) => inner.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OptionalQueryRejection {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::FailedToDeserializeQueryString(inner) => Some(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{routing::post, Router};
|
||||
use http::{header::CONTENT_TYPE, StatusCode};
|
||||
use http::header::CONTENT_TYPE;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[tokio::test]
|
||||
async fn supports_multiple_values() {
|
||||
async fn query_supports_multiple_values() {
|
||||
#[derive(Deserialize)]
|
||||
struct Data {
|
||||
#[serde(rename = "value")]
|
||||
|
@ -149,10 +299,90 @@ mod tests {
|
|||
.post("/?value=one&value=two")
|
||||
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body("")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.text().await, "one,two");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn optional_query_supports_multiple_values() {
|
||||
#[derive(Deserialize)]
|
||||
struct Data {
|
||||
#[serde(rename = "value")]
|
||||
values: Vec<String>,
|
||||
}
|
||||
|
||||
let app = Router::new().route(
|
||||
"/",
|
||||
post(|OptionalQuery(data): OptionalQuery<Data>| async move {
|
||||
data.map(|Data { values }| values.join(","))
|
||||
.unwrap_or("None".to_owned())
|
||||
}),
|
||||
);
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client
|
||||
.post("/?value=one&value=two")
|
||||
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body("")
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.text().await, "one,two");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn optional_query_deserializes_no_parameters_into_none() {
|
||||
#[derive(Deserialize)]
|
||||
struct Data {
|
||||
value: String,
|
||||
}
|
||||
|
||||
let app = Router::new().route(
|
||||
"/",
|
||||
post(|OptionalQuery(data): OptionalQuery<Data>| async move {
|
||||
match data {
|
||||
None => "None".into(),
|
||||
Some(data) => data.value,
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.post("/").body("").await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.text().await, "None");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn optional_query_preserves_parsing_errors() {
|
||||
#[derive(Deserialize)]
|
||||
struct Data {
|
||||
value: String,
|
||||
}
|
||||
|
||||
let app = Router::new().route(
|
||||
"/",
|
||||
post(|OptionalQuery(data): OptionalQuery<Data>| async move {
|
||||
match data {
|
||||
None => "None".into(),
|
||||
Some(data) => data.value,
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client
|
||||
.post("/?other=something")
|
||||
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body("")
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,13 @@ use axum::async_trait;
|
|||
use axum::extract::{FromRequest, FromRequestParts, Request};
|
||||
use axum::response::IntoResponse;
|
||||
use http::request::Parts;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
use crate::routing::TypedPath;
|
||||
|
||||
/// Extractor for customizing extractor rejections
|
||||
///
|
||||
/// `WithRejection` wraps another extractor and gives you the result. If the
|
||||
|
@ -137,15 +140,29 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
impl<E, R> TypedPath for WithRejection<E, R>
|
||||
where
|
||||
E: TypedPath,
|
||||
{
|
||||
const PATH: &'static str = E::PATH;
|
||||
}
|
||||
|
||||
impl<E, R> Display for WithRejection<E, R>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::body::Body;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::Request;
|
||||
use axum::response::Response;
|
||||
use http::request::Parts;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn extractor_rejection_is_transformed() {
|
||||
|
|
|
@ -138,13 +138,13 @@ mod tests {
|
|||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/123").send().await;
|
||||
let res = client.get("/123").await;
|
||||
assert_eq!(res.text().await, "123");
|
||||
|
||||
let res = client.get("/foo?a=bar").send().await;
|
||||
let res = client.get("/foo?a=bar").await;
|
||||
assert_eq!(res.text().await, "bar");
|
||||
|
||||
let res = client.get("/foo").send().await;
|
||||
let res = client.get("/foo").await;
|
||||
assert_eq!(res.text().await, "fallback");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ pin_project! {
|
|||
/// JsonLines::new(stream_of_values()).into_response()
|
||||
/// }
|
||||
/// ```
|
||||
// we use `AsExtractor` as the default because you're more likely to name this type if its used
|
||||
// we use `AsExtractor` as the default because you're more likely to name this type if it's used
|
||||
// as an extractor
|
||||
#[must_use]
|
||||
pub struct JsonLines<S, T = AsExtractor> {
|
||||
|
@ -111,8 +111,8 @@ where
|
|||
// `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 stream = TryStreamExt::map_err(body, |err| io::Error::new(io::ErrorKind::Other, err));
|
||||
let stream = body.into_data_stream();
|
||||
let stream = stream.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
|
||||
let read = StreamReader::new(stream);
|
||||
let lines_stream = LinesStream::new(read.lines());
|
||||
|
||||
|
@ -184,7 +184,7 @@ mod tests {
|
|||
use futures_util::StreamExt;
|
||||
use http::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use std::{convert::Infallible, error::Error};
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
|
||||
struct User {
|
||||
|
@ -224,7 +224,6 @@ mod tests {
|
|||
]
|
||||
.join("\n"),
|
||||
)
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
@ -245,7 +244,7 @@ mod tests {
|
|||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/").send().await;
|
||||
let res = client.get("/").await;
|
||||
|
||||
let values = res
|
||||
.text()
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
//! `cookie-key-expansion` | Enables the `Key::derive_from` method | No
|
||||
//! `erased-json` | Enables the `ErasedJson` response | No
|
||||
//! `form` | Enables the `Form` extractor | No
|
||||
//! `json-deserializer` | Enables the `JsonDeserializer` extractor | No
|
||||
//! `json-lines` | Enables the `JsonLines` extractor and response | No
|
||||
//! `multipart` | Enables the `Multpart` extractor | No
|
||||
//! `multipart` | Enables the `Multipart` extractor | No
|
||||
//! `protobuf` | Enables the `Protobuf` extractor and response | No
|
||||
//! `query` | Enables the `Query` extractor | No
|
||||
//! `tracing` | Log rejections from built-in extractors | Yes
|
||||
//! `typed-routing` | Enables the `TypedPath` routing utilities | No
|
||||
//! `typed-header` | Enables the `TypedHeader` extractor and response | No
|
||||
//!
|
||||
|
@ -60,7 +62,7 @@
|
|||
missing_debug_implementations,
|
||||
missing_docs
|
||||
)]
|
||||
#![deny(unreachable_pub, private_in_public)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![allow(elided_lifetimes_in_paths, clippy::type_complexity)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
|
||||
|
@ -95,11 +97,10 @@ pub use typed_header::TypedHeader;
|
|||
#[cfg(feature = "protobuf")]
|
||||
pub mod protobuf;
|
||||
|
||||
/// _not_ public API
|
||||
#[cfg(feature = "typed-routing")]
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
//! _not_ public API
|
||||
|
||||
use percent_encoding::{AsciiSet, CONTROLS};
|
||||
|
||||
pub use percent_encoding::utf8_percent_encode;
|
||||
|
@ -114,9 +115,8 @@ pub mod __private {
|
|||
use axum_macros::__private_axum_test as test;
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) mod test_helpers {
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use axum::{extract::Request, response::Response, serve};
|
||||
|
||||
mod test_client {
|
||||
|
|
|
@ -201,7 +201,6 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{routing::post, Router};
|
||||
use http::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn decode_body() {
|
||||
|
@ -221,7 +220,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client.post("/").body(input.encode_to_vec()).send().await;
|
||||
let res = client.post("/").body(input.encode_to_vec()).await;
|
||||
|
||||
let body = res.text().await;
|
||||
|
||||
|
@ -249,7 +248,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client.post("/").body(input.encode_to_vec()).send().await;
|
||||
let res = client.post("/").body(input.encode_to_vec()).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
@ -284,7 +283,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client.post("/").body(input.encode_to_vec()).send().await;
|
||||
let res = client.post("/").body(input.encode_to_vec()).await;
|
||||
|
||||
assert_eq!(
|
||||
res.headers()["content-type"],
|
||||
|
|
|
@ -342,7 +342,7 @@ mod sealed {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{extract::Path, http::StatusCode, routing::get};
|
||||
use axum::{extract::Path, routing::get};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tsr() {
|
||||
|
@ -352,17 +352,17 @@ mod tests {
|
|||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/foo").send().await;
|
||||
let res = client.get("/foo").await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client.get("/foo/").send().await;
|
||||
let res = client.get("/foo/").await;
|
||||
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
|
||||
assert_eq!(res.headers()["location"], "/foo");
|
||||
|
||||
let res = client.get("/bar/").send().await;
|
||||
let res = client.get("/bar/").await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client.get("/bar").send().await;
|
||||
let res = client.get("/bar").await;
|
||||
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
|
||||
assert_eq!(res.headers()["location"], "/bar/");
|
||||
}
|
||||
|
@ -381,19 +381,19 @@ mod tests {
|
|||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/a/foo").send().await;
|
||||
let res = client.get("/a/foo").await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.text().await, "foo");
|
||||
|
||||
let res = client.get("/a/foo/").send().await;
|
||||
let res = client.get("/a/foo/").await;
|
||||
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
|
||||
assert_eq!(res.headers()["location"], "/a/foo");
|
||||
|
||||
let res = client.get("/b/foo/").send().await;
|
||||
let res = client.get("/b/foo/").await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.text().await, "foo");
|
||||
|
||||
let res = client.get("/b/foo").send().await;
|
||||
let res = client.get("/b/foo").await;
|
||||
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
|
||||
assert_eq!(res.headers()["location"], "/b/foo/");
|
||||
}
|
||||
|
@ -404,7 +404,7 @@ mod tests {
|
|||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/foo/?a=a").send().await;
|
||||
let res = client.get("/foo/?a=a").await;
|
||||
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
|
||||
assert_eq!(res.headers()["location"], "/foo?a=a");
|
||||
}
|
||||
|
|
|
@ -149,8 +149,9 @@ impl<S> From<Resource<S>> for Router<S> {
|
|||
mod tests {
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
use axum::{body::Body, extract::Path, http::Method, Router};
|
||||
use axum::{body::Body, extract::Path, http::Method};
|
||||
use http::Request;
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -216,7 +217,7 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bytes = hyper::body::to_bytes(res).await.unwrap();
|
||||
let bytes = res.collect().await.unwrap().to_bytes();
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,7 +321,7 @@ where
|
|||
/// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
|
||||
/// given type.
|
||||
///
|
||||
/// If you see it in type errors its most likely because the second argument to your handler doesn't
|
||||
/// If you see it in type errors it's most likely because the second argument to your handler doesn't
|
||||
/// implement [`TypedPath`].
|
||||
///
|
||||
/// You normally shouldn't have to use this trait directly.
|
||||
|
@ -386,7 +386,15 @@ impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::routing::TypedPath;
|
||||
use crate::{
|
||||
extract::WithRejection,
|
||||
routing::{RouterExt, TypedPath},
|
||||
};
|
||||
use axum::{
|
||||
extract::rejection::PathRejection,
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
|
@ -434,4 +442,25 @@ mod tests {
|
|||
|
||||
assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true&qux=1337");
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // just needs to compile
|
||||
fn supports_with_rejection() {
|
||||
async fn handler(_: WithRejection<UsersShow, MyRejection>) {}
|
||||
|
||||
struct MyRejection {}
|
||||
|
||||
impl IntoResponse for MyRejection {
|
||||
fn into_response(self) -> Response {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathRejection> for MyRejection {
|
||||
fn from(_: PathRejection) -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let _: Router = Router::new().typed_get(handler);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use axum::{
|
|||
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
|
||||
};
|
||||
use headers::{Header, HeaderMapExt};
|
||||
use http::request::Parts;
|
||||
use http::{request::Parts, StatusCode};
|
||||
use std::convert::Infallible;
|
||||
|
||||
/// Extractor and response that works with typed header values from [`headers`].
|
||||
|
@ -105,7 +105,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Rejection used for [`TypedHeader`](TypedHeader).
|
||||
/// Rejection used for [`TypedHeader`].
|
||||
#[cfg(feature = "typed-header")]
|
||||
#[derive(Debug)]
|
||||
pub struct TypedHeaderRejection {
|
||||
|
@ -123,6 +123,14 @@ impl TypedHeaderRejection {
|
|||
pub fn reason(&self) -> &TypedHeaderRejectionReason {
|
||||
&self.reason
|
||||
}
|
||||
|
||||
/// Returns `true` if the typed header rejection reason is [`Missing`].
|
||||
///
|
||||
/// [`Missing`]: TypedHeaderRejectionReason::Missing
|
||||
#[must_use]
|
||||
pub fn is_missing(&self) -> bool {
|
||||
self.reason.is_missing()
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional information regarding a [`TypedHeaderRejection`]
|
||||
|
@ -136,9 +144,22 @@ pub enum TypedHeaderRejectionReason {
|
|||
Error(headers::Error),
|
||||
}
|
||||
|
||||
impl TypedHeaderRejectionReason {
|
||||
/// Returns `true` if the typed header rejection reason is [`Missing`].
|
||||
///
|
||||
/// [`Missing`]: TypedHeaderRejectionReason::Missing
|
||||
#[must_use]
|
||||
pub fn is_missing(&self) -> bool {
|
||||
matches!(self, Self::Missing)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for TypedHeaderRejection {
|
||||
fn into_response(self) -> Response {
|
||||
(http::StatusCode::BAD_REQUEST, self.to_string()).into_response()
|
||||
let status = StatusCode::BAD_REQUEST;
|
||||
let body = self.to_string();
|
||||
axum_core::__log_rejection!(rejection_type = Self, body_text = body, status = status,);
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,7 +189,7 @@ impl std::error::Error for TypedHeaderRejection {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::*;
|
||||
use axum::{response::IntoResponse, routing::get, Router};
|
||||
use axum::{routing::get, Router};
|
||||
|
||||
#[tokio::test]
|
||||
async fn typed_header() {
|
||||
|
@ -190,7 +211,6 @@ mod tests {
|
|||
.header("user-agent", "foobar")
|
||||
.header("cookie", "a=1; b=2")
|
||||
.header("cookie", "c=3")
|
||||
.send()
|
||||
.await;
|
||||
let body = res.text().await;
|
||||
assert_eq!(
|
||||
|
@ -198,11 +218,11 @@ mod tests {
|
|||
r#"User-Agent="foobar", Cookie=[("a", "1"), ("b", "2"), ("c", "3")]"#
|
||||
);
|
||||
|
||||
let res = client.get("/").header("user-agent", "foobar").send().await;
|
||||
let res = client.get("/").header("user-agent", "foobar").await;
|
||||
let body = res.text().await;
|
||||
assert_eq!(body, r#"User-Agent="foobar", Cookie=[]"#);
|
||||
|
||||
let res = client.get("/").header("cookie", "a=1").send().await;
|
||||
let res = client.get("/").header("cookie", "a=1").await;
|
||||
let body = res.text().await;
|
||||
assert_eq!(body, "Header of type `user-agent` was missing");
|
||||
}
|
||||
|
|
|
@ -7,11 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
# Unreleased
|
||||
|
||||
- **added:** Add `#[debug_middleware]` ([#1993], [#2725])
|
||||
|
||||
[#1993]: https://github.com/tokio-rs/axum/pull/1993
|
||||
[#2725]: https://github.com/tokio-rs/axum/pull/2725
|
||||
|
||||
# 0.4.1 (13. January, 2024)
|
||||
|
||||
- **fixed:** Improve `debug_handler` on tuple response types ([#2201])
|
||||
|
||||
[#2201]: https://github.com/tokio-rs/axum/pull/2201
|
||||
|
||||
# 0.4.0 (27. November, 2023)
|
||||
|
||||
- **breaking:** `#[debug_handler]` no longer accepts a `body = _` argument. The
|
||||
body type is always `axum::body::Body` ([#1751])
|
||||
- **fixed:** Fix `rust-version` specific in Cargo.toml ([#2204])
|
||||
|
||||
[#2204]: https://github.com/tokio-rs/axum/pull/2204
|
||||
[#1751]: https://github.com/tokio-rs/axum/pull/1751
|
||||
|
||||
# 0.3.8 (17. July, 2023)
|
||||
|
||||
- **fixed:** Allow unreachable code in `#[debug_handler]` ([#2014])
|
||||
|
||||
[#1751]: https://github.com/tokio-rs/axum/pull/1751
|
||||
[#2014]: https://github.com/tokio-rs/axum/pull/2014
|
||||
|
||||
# 0.3.7 (22. March, 2023)
|
||||
|
@ -28,7 +47,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
request-consuming extractors ([#1826])
|
||||
|
||||
[#1826]: https://github.com/tokio-rs/axum/pull/1826
|
||||
[#1751]: https://github.com/tokio-rs/axum/pull/1751
|
||||
|
||||
# 0.3.5 (03. March, 2023)
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
categories = ["asynchronous", "network-programming", "web-programming"]
|
||||
description = "Macros for axum"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
rust-version = "1.66"
|
||||
homepage = "https://github.com/tokio-rs/axum"
|
||||
keywords = ["axum"]
|
||||
license = "MIT"
|
||||
name = "axum-macros"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tokio-rs/axum"
|
||||
version = "0.3.7" # remember to also bump the version that axum and axum-extra depends on
|
||||
version = "0.4.1" # remember to also bump the version that axum and axum-extra depends on
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
@ -30,8 +30,8 @@ syn = { version = "2.0", features = [
|
|||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { path = "../axum", version = "0.6.0", features = ["macros"] }
|
||||
axum-extra = { path = "../axum-extra", version = "0.7.0", features = ["typed-routing", "cookie-private", "typed-header"] }
|
||||
axum = { path = "../axum", version = "0.7.2", features = ["macros"] }
|
||||
axum-extra = { path = "../axum-extra", version = "0.9.0", features = ["typed-routing", "cookie-private", "typed-header"] }
|
||||
rustversion = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -41,3 +41,7 @@ trybuild = "1.0.63"
|
|||
|
||||
[package.metadata.cargo-public-api-crates]
|
||||
allowed = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
|
@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
|
|||
|
||||
## Minimum supported Rust version
|
||||
|
||||
axum-macros's MSRV is 1.63.
|
||||
axum-macros's MSRV is 1.66.
|
||||
|
||||
## Getting Help
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
nightly-2023-04-06
|
||||
nightly-2024-03-13
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
use std::collections::HashSet;
|
||||
use std::{collections::HashSet, fmt};
|
||||
|
||||
use crate::{
|
||||
attr_parsing::{parse_assignment_attribute, second},
|
||||
with_position::{Position, WithPosition},
|
||||
};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, Token, Type};
|
||||
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, ReturnType, Token, Type};
|
||||
|
||||
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
|
||||
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenStream {
|
||||
let Attrs { state_ty } = attr;
|
||||
|
||||
let mut state_ty = state_ty.map(second);
|
||||
|
||||
let check_extractor_count = check_extractor_count(&item_fn);
|
||||
let check_path_extractor = check_path_extractor(&item_fn);
|
||||
let check_output_impls_into_response = check_output_impls_into_response(&item_fn);
|
||||
let check_extractor_count = check_extractor_count(&item_fn, kind);
|
||||
let check_path_extractor = check_path_extractor(&item_fn, kind);
|
||||
let check_output_tuples = check_output_tuples(&item_fn);
|
||||
let check_output_impls_into_response = if check_output_tuples.is_empty() {
|
||||
check_output_impls_into_response(&item_fn)
|
||||
} else {
|
||||
check_output_tuples
|
||||
};
|
||||
|
||||
// If the function is generic, we can't reliably check its inputs or whether the future it
|
||||
// returns is `Send`. Skip those checks to avoid unhelpful additional compiler errors.
|
||||
|
@ -32,8 +37,10 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
|
|||
err = Some(
|
||||
syn::Error::new(
|
||||
Span::call_site(),
|
||||
"can't infer state type, please add set it explicitly, as in \
|
||||
`#[debug_handler(state = MyStateType)]`",
|
||||
format!(
|
||||
"can't infer state type, please add set it explicitly, as in \
|
||||
`#[axum_macros::debug_{kind}(state = MyStateType)]`"
|
||||
),
|
||||
)
|
||||
.into_compile_error(),
|
||||
);
|
||||
|
@ -43,16 +50,16 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
|
|||
err.unwrap_or_else(|| {
|
||||
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(()));
|
||||
|
||||
let check_future_send = check_future_send(&item_fn);
|
||||
let check_future_send = check_future_send(&item_fn, kind);
|
||||
|
||||
if let Some(check_input_order) = check_input_order(&item_fn) {
|
||||
if let Some(check_input_order) = check_input_order(&item_fn, kind) {
|
||||
quote! {
|
||||
#check_input_order
|
||||
#check_future_send
|
||||
}
|
||||
} else {
|
||||
let check_inputs_impls_from_request =
|
||||
check_inputs_impls_from_request(&item_fn, state_ty);
|
||||
check_inputs_impls_from_request(&item_fn, state_ty, kind);
|
||||
|
||||
quote! {
|
||||
#check_inputs_impls_from_request
|
||||
|
@ -63,17 +70,45 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
|
|||
} else {
|
||||
syn::Error::new_spanned(
|
||||
&item_fn.sig.generics,
|
||||
"`#[axum_macros::debug_handler]` doesn't support generic functions",
|
||||
format!("`#[axum_macros::debug_{kind}]` doesn't support generic functions"),
|
||||
)
|
||||
.into_compile_error()
|
||||
};
|
||||
|
||||
let middleware_takes_next_as_last_arg =
|
||||
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
|
||||
|
||||
quote! {
|
||||
#item_fn
|
||||
#check_extractor_count
|
||||
#check_path_extractor
|
||||
#check_output_impls_into_response
|
||||
#check_inputs_and_future_send
|
||||
#middleware_takes_next_as_last_arg
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum FunctionKind {
|
||||
Handler,
|
||||
Middleware,
|
||||
}
|
||||
|
||||
impl fmt::Display for FunctionKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FunctionKind::Handler => f.write_str("handler"),
|
||||
FunctionKind::Middleware => f.write_str("middleware"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionKind {
|
||||
fn name_uppercase_plural(&self) -> &'static str {
|
||||
match self {
|
||||
FunctionKind::Handler => "Handlers",
|
||||
FunctionKind::Middleware => "Middleware",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,25 +140,36 @@ impl Parse for Attrs {
|
|||
}
|
||||
}
|
||||
|
||||
fn check_extractor_count(item_fn: &ItemFn) -> Option<TokenStream> {
|
||||
fn check_extractor_count(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
|
||||
let max_extractors = 16;
|
||||
if item_fn.sig.inputs.len() <= max_extractors {
|
||||
let inputs = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind))
|
||||
.count();
|
||||
if inputs <= max_extractors {
|
||||
None
|
||||
} else {
|
||||
let error_message = format!(
|
||||
"Handlers cannot take more than {max_extractors} arguments. \
|
||||
"{} cannot take more than {max_extractors} arguments. \
|
||||
Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors",
|
||||
kind.name_uppercase_plural(),
|
||||
);
|
||||
let error = syn::Error::new_spanned(&item_fn.sig.inputs, error_message).to_compile_error();
|
||||
Some(error)
|
||||
}
|
||||
}
|
||||
|
||||
fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
|
||||
fn extractor_idents(
|
||||
item_fn: &ItemFn,
|
||||
kind: FunctionKind,
|
||||
) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
|
||||
item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(move |arg| skip_next_arg(arg, kind))
|
||||
.enumerate()
|
||||
.filter_map(|(idx, fn_arg)| match fn_arg {
|
||||
FnArg::Receiver(_) => None,
|
||||
|
@ -141,8 +187,8 @@ fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnAr
|
|||
})
|
||||
}
|
||||
|
||||
fn check_path_extractor(item_fn: &ItemFn) -> TokenStream {
|
||||
let path_extractors = extractor_idents(item_fn)
|
||||
fn check_path_extractor(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
|
||||
let path_extractors = extractor_idents(item_fn, kind)
|
||||
.filter(|(_, _, ident)| *ident == "Path")
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -174,121 +220,294 @@ fn is_self_pat_type(typed: &syn::PatType) -> bool {
|
|||
ident == "self"
|
||||
}
|
||||
|
||||
fn check_inputs_impls_from_request(item_fn: &ItemFn, state_ty: Type) -> TokenStream {
|
||||
fn check_inputs_impls_from_request(
|
||||
item_fn: &ItemFn,
|
||||
state_ty: Type,
|
||||
kind: FunctionKind,
|
||||
) -> TokenStream {
|
||||
let takes_self = item_fn.sig.inputs.first().map_or(false, |arg| match arg {
|
||||
FnArg::Receiver(_) => true,
|
||||
FnArg::Typed(typed) => is_self_pat_type(typed),
|
||||
});
|
||||
|
||||
WithPosition::new(item_fn.sig.inputs.iter())
|
||||
.enumerate()
|
||||
.map(|(idx, arg)| {
|
||||
let must_impl_from_request_parts = match &arg {
|
||||
Position::First(_) | Position::Middle(_) => true,
|
||||
Position::Last(_) | Position::Only(_) => false,
|
||||
};
|
||||
WithPosition::new(
|
||||
item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind)),
|
||||
)
|
||||
.enumerate()
|
||||
.map(|(idx, arg)| {
|
||||
let must_impl_from_request_parts = match &arg {
|
||||
Position::First(_) | Position::Middle(_) => true,
|
||||
Position::Last(_) | Position::Only(_) => false,
|
||||
};
|
||||
|
||||
let arg = arg.into_inner();
|
||||
let arg = arg.into_inner();
|
||||
|
||||
let (span, ty) = match arg {
|
||||
FnArg::Receiver(receiver) => {
|
||||
if receiver.reference.is_some() {
|
||||
return syn::Error::new_spanned(
|
||||
receiver,
|
||||
"Handlers must only take owned values",
|
||||
)
|
||||
.into_compile_error();
|
||||
}
|
||||
let (span, ty) = match arg {
|
||||
FnArg::Receiver(receiver) => {
|
||||
if receiver.reference.is_some() {
|
||||
return syn::Error::new_spanned(
|
||||
receiver,
|
||||
"Handlers must only take owned values",
|
||||
)
|
||||
.into_compile_error();
|
||||
}
|
||||
|
||||
let span = receiver.span();
|
||||
let span = receiver.span();
|
||||
(span, syn::parse_quote!(Self))
|
||||
}
|
||||
FnArg::Typed(typed) => {
|
||||
let ty = &typed.ty;
|
||||
let span = ty.span();
|
||||
|
||||
if is_self_pat_type(typed) {
|
||||
(span, syn::parse_quote!(Self))
|
||||
}
|
||||
FnArg::Typed(typed) => {
|
||||
let ty = &typed.ty;
|
||||
let span = ty.span();
|
||||
|
||||
if is_self_pat_type(typed) {
|
||||
(span, syn::parse_quote!(Self))
|
||||
} else {
|
||||
(span, ty.clone())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let consumes_request = request_consuming_type_name(&ty).is_some();
|
||||
|
||||
let check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_call_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn_body = if takes_self {
|
||||
quote_spanned! {span=>
|
||||
Self::#check_fn();
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#check_fn();
|
||||
}
|
||||
};
|
||||
|
||||
let check_fn_generics = if must_impl_from_request_parts || consumes_request {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { <M> }
|
||||
};
|
||||
|
||||
let from_request_bound = if must_impl_from_request_parts {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
|
||||
}
|
||||
} else if consumes_request {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty> + Send
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty, M> + Send
|
||||
}
|
||||
};
|
||||
|
||||
quote_spanned! {span=>
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #check_fn #check_fn_generics()
|
||||
where
|
||||
#from_request_bound,
|
||||
{}
|
||||
|
||||
// we have to call the function to actually trigger a compile error
|
||||
// since the function is generic, just defining it is not enough
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #call_check_fn()
|
||||
{
|
||||
#call_check_fn_body
|
||||
} else {
|
||||
(span, ty.clone())
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>()
|
||||
};
|
||||
|
||||
let consumes_request = request_consuming_type_name(&ty).is_some();
|
||||
|
||||
let check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_call_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn_body = if takes_self {
|
||||
quote_spanned! {span=>
|
||||
Self::#check_fn();
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#check_fn();
|
||||
}
|
||||
};
|
||||
|
||||
let check_fn_generics = if must_impl_from_request_parts || consumes_request {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { <M> }
|
||||
};
|
||||
|
||||
let from_request_bound = if must_impl_from_request_parts {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
|
||||
}
|
||||
} else if consumes_request {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty> + Send
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty, M> + Send
|
||||
}
|
||||
};
|
||||
|
||||
quote_spanned! {span=>
|
||||
#[allow(warnings)]
|
||||
#[doc(hidden)]
|
||||
fn #check_fn #check_fn_generics()
|
||||
where
|
||||
#from_request_bound,
|
||||
{}
|
||||
|
||||
// we have to call the function to actually trigger a compile error
|
||||
// since the function is generic, just defining it is not enough
|
||||
#[allow(warnings)]
|
||||
#[doc(hidden)]
|
||||
fn #call_check_fn()
|
||||
{
|
||||
#call_check_fn_body
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>()
|
||||
}
|
||||
|
||||
fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
|
||||
fn check_output_tuples(item_fn: &ItemFn) -> TokenStream {
|
||||
let elems = match &item_fn.sig.output {
|
||||
ReturnType::Type(_, ty) => match &**ty {
|
||||
Type::Tuple(tuple) => &tuple.elems,
|
||||
_ => return quote! {},
|
||||
},
|
||||
ReturnType::Default => return quote! {},
|
||||
};
|
||||
|
||||
let handler_ident = &item_fn.sig.ident;
|
||||
|
||||
match elems.len() {
|
||||
0 => quote! {},
|
||||
n if n > 17 => syn::Error::new_spanned(
|
||||
&item_fn.sig.output,
|
||||
"Cannot return tuples with more than 17 elements",
|
||||
)
|
||||
.to_compile_error(),
|
||||
_ => WithPosition::new(elems)
|
||||
.enumerate()
|
||||
.map(|(idx, arg)| match arg {
|
||||
Position::First(ty) => match extract_clean_typename(ty).as_deref() {
|
||||
Some("StatusCode" | "Response") => quote! {},
|
||||
Some("Parts") => check_is_response_parts(ty, handler_ident, idx),
|
||||
Some(_) | None => {
|
||||
if let Some(tn) = well_known_last_response_type(ty) {
|
||||
syn::Error::new_spanned(
|
||||
ty,
|
||||
format!(
|
||||
"`{tn}` must be the last element \
|
||||
in a response tuple"
|
||||
),
|
||||
)
|
||||
.to_compile_error()
|
||||
} else {
|
||||
check_into_response_parts(ty, handler_ident, idx)
|
||||
}
|
||||
}
|
||||
},
|
||||
Position::Middle(ty) => {
|
||||
if let Some(tn) = well_known_last_response_type(ty) {
|
||||
syn::Error::new_spanned(
|
||||
ty,
|
||||
format!("`{tn}` must be the last element in a response tuple"),
|
||||
)
|
||||
.to_compile_error()
|
||||
} else {
|
||||
check_into_response_parts(ty, handler_ident, idx)
|
||||
}
|
||||
}
|
||||
Position::Last(ty) | Position::Only(ty) => check_into_response(handler_ident, ty),
|
||||
})
|
||||
.collect::<TokenStream>(),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_into_response(handler: &Ident, ty: &Type) -> TokenStream {
|
||||
let (span, ty) = (ty.span(), ty.clone());
|
||||
|
||||
let check_fn = format_ident!(
|
||||
"__axum_macros_check_{handler}_into_response_check",
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn = format_ident!(
|
||||
"__axum_macros_check_{handler}_into_response_call_check",
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn_body = quote_spanned! {span=>
|
||||
#check_fn();
|
||||
};
|
||||
|
||||
let from_request_bound = quote_spanned! {span=>
|
||||
#ty: ::axum::response::IntoResponse
|
||||
};
|
||||
quote_spanned! {span=>
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #check_fn()
|
||||
where
|
||||
#from_request_bound,
|
||||
{}
|
||||
|
||||
// we have to call the function to actually trigger a compile error
|
||||
// since the function is generic, just defining it is not enough
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #call_check_fn() {
|
||||
#call_check_fn_body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_is_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream {
|
||||
let (span, ty) = (ty.span(), ty.clone());
|
||||
|
||||
let check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_is_response_parts_{index}_check",
|
||||
ident,
|
||||
span = span,
|
||||
);
|
||||
|
||||
quote_spanned! {span=>
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #check_fn(parts: #ty) -> ::axum::http::response::Parts {
|
||||
parts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_into_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream {
|
||||
let (span, ty) = (ty.span(), ty.clone());
|
||||
|
||||
let check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_into_response_parts_{index}_check",
|
||||
ident,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_into_response_parts_{index}_call_check",
|
||||
ident,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn_body = quote_spanned! {span=>
|
||||
#check_fn();
|
||||
};
|
||||
|
||||
let from_request_bound = quote_spanned! {span=>
|
||||
#ty: ::axum::response::IntoResponseParts
|
||||
};
|
||||
quote_spanned! {span=>
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #check_fn()
|
||||
where
|
||||
#from_request_bound,
|
||||
{}
|
||||
|
||||
// we have to call the function to actually trigger a compile error
|
||||
// since the function is generic, just defining it is not enough
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #call_check_fn() {
|
||||
#call_check_fn_body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_input_order(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
|
||||
let number_of_inputs = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind))
|
||||
.count();
|
||||
|
||||
let types_that_consume_the_request = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind))
|
||||
.enumerate()
|
||||
.filter_map(|(idx, arg)| {
|
||||
let ty = match arg {
|
||||
|
@ -308,7 +527,7 @@ fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
|
|||
// exactly one type that consumes the request
|
||||
if types_that_consume_the_request.len() == 1 {
|
||||
// and that is not the last
|
||||
if types_that_consume_the_request[0].0 != item_fn.sig.inputs.len() - 1 {
|
||||
if types_that_consume_the_request[0].0 != number_of_inputs - 1 {
|
||||
let (_idx, type_name, span) = &types_that_consume_the_request[0];
|
||||
let error = format!(
|
||||
"`{type_name}` consumes the request body and thus must be \
|
||||
|
@ -334,7 +553,7 @@ fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
|
|||
compile_error!(#error);
|
||||
})
|
||||
} else {
|
||||
let types = WithPosition::new(types_that_consume_the_request.into_iter())
|
||||
let types = WithPosition::new(types_that_consume_the_request)
|
||||
.map(|pos| match pos {
|
||||
Position::First((_, type_name, _)) | Position::Middle((_, type_name, _)) => {
|
||||
format!("`{type_name}`, ")
|
||||
|
@ -355,18 +574,18 @@ fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
|
|||
}
|
||||
}
|
||||
|
||||
fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
|
||||
fn extract_clean_typename(ty: &Type) -> Option<String> {
|
||||
let path = match ty {
|
||||
Type::Path(type_path) => &type_path.path,
|
||||
_ => return None,
|
||||
};
|
||||
path.segments.last().map(|p| p.ident.to_string())
|
||||
}
|
||||
|
||||
let ident = match path.segments.last() {
|
||||
Some(path_segment) => &path_segment.ident,
|
||||
None => return None,
|
||||
};
|
||||
fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
|
||||
let typename = extract_clean_typename(ty)?;
|
||||
|
||||
let type_name = match &*ident.to_string() {
|
||||
let type_name = match &*typename {
|
||||
"Json" => "Json<_>",
|
||||
"RawBody" => "RawBody<_>",
|
||||
"RawForm" => "RawForm",
|
||||
|
@ -384,6 +603,25 @@ fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
|
|||
Some(type_name)
|
||||
}
|
||||
|
||||
fn well_known_last_response_type(ty: &Type) -> Option<&'static str> {
|
||||
let typename = match extract_clean_typename(ty) {
|
||||
Some(tn) => tn,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let type_name = match &*typename {
|
||||
"Json" => "Json<_>",
|
||||
"Protobuf" => "Protobuf",
|
||||
"JsonLines" => "JsonLines<_>",
|
||||
"Form" => "Form<_>",
|
||||
"Bytes" => "Bytes",
|
||||
"String" => "String",
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(type_name)
|
||||
}
|
||||
|
||||
fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
|
||||
let ty = match &item_fn.sig.output {
|
||||
syn::ReturnType::Default => return quote! {},
|
||||
|
@ -473,13 +711,13 @@ fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
fn check_future_send(item_fn: &ItemFn) -> TokenStream {
|
||||
fn check_future_send(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
|
||||
if item_fn.sig.asyncness.is_none() {
|
||||
match &item_fn.sig.output {
|
||||
syn::ReturnType::Default => {
|
||||
return syn::Error::new_spanned(
|
||||
item_fn.sig.fn_token,
|
||||
"Handlers must be `async fn`s",
|
||||
format!("{} must be `async fn`s", kind.name_uppercase_plural()),
|
||||
)
|
||||
.into_compile_error();
|
||||
}
|
||||
|
@ -583,7 +821,69 @@ fn state_types_from_args(item_fn: &ItemFn) -> HashSet<Type> {
|
|||
crate::infer_state_types(types).collect()
|
||||
}
|
||||
|
||||
fn next_is_last_input(item_fn: &ItemFn) -> TokenStream {
|
||||
let next_args = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, arg)| !skip_next_arg(arg, FunctionKind::Middleware))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if next_args.is_empty() {
|
||||
return quote! {
|
||||
compile_error!(
|
||||
"Middleware functions must take `axum::middleware::Next` as the last argument",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if next_args.len() == 1 {
|
||||
let (idx, arg) = &next_args[0];
|
||||
if *idx != item_fn.sig.inputs.len() - 1 {
|
||||
return quote_spanned! {arg.span()=>
|
||||
compile_error!("`axum::middleware::Next` must the last argument");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if next_args.len() >= 2 {
|
||||
return quote! {
|
||||
compile_error!(
|
||||
"Middleware functions can only take one argument of type `axum::middleware::Next`",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
quote! {}
|
||||
}
|
||||
|
||||
fn skip_next_arg(arg: &FnArg, kind: FunctionKind) -> bool {
|
||||
match kind {
|
||||
FunctionKind::Handler => true,
|
||||
FunctionKind::Middleware => match arg {
|
||||
FnArg::Receiver(_) => true,
|
||||
FnArg::Typed(pat_type) => {
|
||||
if let Type::Path(type_path) = &*pat_type.ty {
|
||||
type_path
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map_or(true, |path_segment| path_segment.ident != "Next")
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui() {
|
||||
fn ui_debug_handler() {
|
||||
crate::run_ui_tests("debug_handler");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_debug_middleware() {
|
||||
crate::run_ui_tests("debug_middleware");
|
||||
}
|
||||
|
|
|
@ -63,8 +63,7 @@ impl State {
|
|||
/// ```
|
||||
fn trait_generics(&self) -> impl Iterator<Item = Type> {
|
||||
match self {
|
||||
State::Default(inner) => iter::once(inner.clone()),
|
||||
State::Custom(inner) => iter::once(inner.clone()),
|
||||
State::Default(inner) | State::Custom(inner) => iter::once(inner.clone()),
|
||||
State::CannotInfer => iter::once(parse_quote!(S)),
|
||||
}
|
||||
}
|
||||
|
@ -85,8 +84,7 @@ impl State {
|
|||
impl ToTokens for State {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
State::Custom(inner) => inner.to_tokens(tokens),
|
||||
State::Default(inner) => inner.to_tokens(tokens),
|
||||
State::Custom(inner) | State::Default(inner) => inner.to_tokens(tokens),
|
||||
State::CannotInfer => quote! { S }.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
|
@ -1005,7 +1003,7 @@ fn infer_state_type_from_field_attributes(fields: &Fields) -> impl Iterator<Item
|
|||
match fields {
|
||||
Fields::Named(fields_named) => {
|
||||
Box::new(fields_named.named.iter().filter_map(|field| {
|
||||
// TODO(david): its a little wasteful to parse the attributes again here
|
||||
// TODO(david): it's a little wasteful to parse the attributes again here
|
||||
// ideally we should parse things once and pass the data down
|
||||
let FromRequestFieldAttrs { via } =
|
||||
parse_attrs("from_request", &field.attrs).ok()?;
|
||||
|
@ -1015,7 +1013,7 @@ fn infer_state_type_from_field_attributes(fields: &Fields) -> impl Iterator<Item
|
|||
}
|
||||
Fields::Unnamed(fields_unnamed) => {
|
||||
Box::new(fields_unnamed.unnamed.iter().filter_map(|field| {
|
||||
// TODO(david): its a little wasteful to parse the attributes again here
|
||||
// TODO(david): it's a little wasteful to parse the attributes again here
|
||||
// ideally we should parse things once and pass the data down
|
||||
let FromRequestFieldAttrs { via } =
|
||||
parse_attrs("from_request", &field.attrs).ok()?;
|
||||
|
|
|
@ -37,13 +37,14 @@
|
|||
missing_debug_implementations,
|
||||
missing_docs
|
||||
)]
|
||||
#![deny(unreachable_pub, private_in_public)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![allow(elided_lifetimes_in_paths, clippy::type_complexity)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(test, allow(clippy::float_cmp))]
|
||||
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
|
||||
|
||||
use debug_handler::FunctionKind;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{parse::Parse, Type};
|
||||
|
@ -233,6 +234,55 @@ use from_request::Trait::{FromRequest, FromRequestParts};
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Concrete state
|
||||
///
|
||||
/// If the extraction can be done only for a concrete state, that type can be specified with
|
||||
/// `#[from_request(state(YourState))]`:
|
||||
///
|
||||
/// ```
|
||||
/// use axum::extract::{FromRequest, FromRequestParts};
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// struct CustomState;
|
||||
///
|
||||
/// struct MyInnerType;
|
||||
///
|
||||
/// #[axum::async_trait]
|
||||
/// impl FromRequestParts<CustomState> for MyInnerType {
|
||||
/// // ...
|
||||
/// # type Rejection = ();
|
||||
///
|
||||
/// # async fn from_request_parts(
|
||||
/// # _parts: &mut axum::http::request::Parts,
|
||||
/// # _state: &CustomState
|
||||
/// # ) -> Result<Self, Self::Rejection> {
|
||||
/// # todo!()
|
||||
/// # }
|
||||
/// }
|
||||
///
|
||||
/// #[derive(FromRequest)]
|
||||
/// #[from_request(state(CustomState))]
|
||||
/// struct MyExtractor {
|
||||
/// custom: MyInnerType,
|
||||
/// body: String,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This is not needed for a `State<T>` as the type is inferred in that case.
|
||||
///
|
||||
/// ```
|
||||
/// use axum::extract::{FromRequest, FromRequestParts, State};
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// struct CustomState;
|
||||
///
|
||||
/// #[derive(FromRequest)]
|
||||
/// struct MyExtractor {
|
||||
/// custom: State<CustomState>,
|
||||
/// body: String,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # The whole type at once
|
||||
///
|
||||
/// By using `#[from_request(via(...))]` on the container you can extract the whole type at once,
|
||||
|
@ -358,9 +408,9 @@ use from_request::Trait::{FromRequest, FromRequestParts};
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`FromRequest`]: https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html
|
||||
/// [`axum::response::Response`]: https://docs.rs/axum/0.6/axum/response/type.Response.html
|
||||
/// [`axum::extract::rejection::ExtensionRejection`]: https://docs.rs/axum/latest/axum/extract/rejection/enum.ExtensionRejection.html
|
||||
/// [`FromRequest`]: https://docs.rs/axum/0.7/axum/extract/trait.FromRequest.html
|
||||
/// [`axum::response::Response`]: https://docs.rs/axum/0.7/axum/response/type.Response.html
|
||||
/// [`axum::extract::rejection::ExtensionRejection`]: https://docs.rs/axum/0.7/axum/extract/rejection/enum.ExtensionRejection.html
|
||||
#[proc_macro_derive(FromRequest, attributes(from_request))]
|
||||
pub fn derive_from_request(item: TokenStream) -> TokenStream {
|
||||
expand_with(item, |item| from_request::expand(item, FromRequest))
|
||||
|
@ -409,13 +459,13 @@ pub fn derive_from_request(item: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// Use `#[derive(FromRequest)]` for that.
|
||||
///
|
||||
/// [`FromRequestParts`]: https://docs.rs/axum/0.6/axum/extract/trait.FromRequestParts.html
|
||||
/// [`FromRequestParts`]: https://docs.rs/axum/0.7/axum/extract/trait.FromRequestParts.html
|
||||
#[proc_macro_derive(FromRequestParts, attributes(from_request))]
|
||||
pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
||||
expand_with(item, |item| from_request::expand(item, FromRequestParts))
|
||||
}
|
||||
|
||||
/// Generates better error messages when applied handler functions.
|
||||
/// Generates better error messages when applied to handler functions.
|
||||
///
|
||||
/// While using [`axum`], you can get long error messages for simple mistakes. For example:
|
||||
///
|
||||
|
@ -466,17 +516,15 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// As the error message says, handler function needs to be async.
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use axum::{routing::get, Router, debug_handler};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// # async {
|
||||
/// let app = Router::new().route("/", get(handler));
|
||||
///
|
||||
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
/// axum::serve(listener, app).await.unwrap();
|
||||
/// # };
|
||||
/// }
|
||||
///
|
||||
/// #[debug_handler]
|
||||
|
@ -559,9 +607,9 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// This macro has no effect when compiled with the release profile. (eg. `cargo build --release`)
|
||||
///
|
||||
/// [`axum`]: https://docs.rs/axum/latest
|
||||
/// [`Handler`]: https://docs.rs/axum/latest/axum/handler/trait.Handler.html
|
||||
/// [`axum::extract::State`]: https://docs.rs/axum/0.6/axum/extract/struct.State.html
|
||||
/// [`axum`]: https://docs.rs/axum/0.7
|
||||
/// [`Handler`]: https://docs.rs/axum/0.7/axum/handler/trait.Handler.html
|
||||
/// [`axum::extract::State`]: https://docs.rs/axum/0.7/axum/extract/struct.State.html
|
||||
/// [`debug_handler`]: macro@debug_handler
|
||||
#[proc_macro_attribute]
|
||||
pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
@ -569,7 +617,65 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
return input;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
return expand_attr_with(_attr, input, debug_handler::expand);
|
||||
return expand_attr_with(_attr, input, |attrs, item_fn| {
|
||||
debug_handler::expand(attrs, item_fn, FunctionKind::Handler)
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates better error messages when applied to middleware functions.
|
||||
///
|
||||
/// This works similarly to [`#[debug_handler]`](macro@debug_handler) except for middleware using
|
||||
/// [`axum::middleware::from_fn`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use axum::{
|
||||
/// routing::get,
|
||||
/// extract::Request,
|
||||
/// response::Response,
|
||||
/// Router,
|
||||
/// middleware::{self, Next},
|
||||
/// debug_middleware,
|
||||
/// };
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let app = Router::new()
|
||||
/// .route("/", get(|| async {}))
|
||||
/// .layer(middleware::from_fn(my_middleware));
|
||||
///
|
||||
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
/// axum::serve(listener, app).await.unwrap();
|
||||
/// }
|
||||
///
|
||||
/// // if this wasn't a valid middleware function #[debug_middleware] would
|
||||
/// // improve compile error
|
||||
/// #[debug_middleware]
|
||||
/// async fn my_middleware(
|
||||
/// request: Request,
|
||||
/// next: Next,
|
||||
/// ) -> Response {
|
||||
/// next.run(request).await
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// This macro has no effect when compiled with the release profile. (eg. `cargo build --release`)
|
||||
///
|
||||
/// [`axum`]: https://docs.rs/axum/latest
|
||||
/// [`axum::middleware::from_fn`]: https://docs.rs/axum/0.7/axum/middleware/fn.from_fn.html
|
||||
/// [`debug_middleware`]: macro@debug_middleware
|
||||
#[proc_macro_attribute]
|
||||
pub fn debug_middleware(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
#[cfg(not(debug_assertions))]
|
||||
return input;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
return expand_attr_with(_attr, input, |attrs, item_fn| {
|
||||
debug_handler::expand(attrs, item_fn, FunctionKind::Middleware)
|
||||
});
|
||||
}
|
||||
|
||||
/// Private API: Do no use this!
|
||||
|
@ -642,7 +748,7 @@ pub fn derive_typed_path(input: TokenStream) -> TokenStream {
|
|||
/// # let _: axum::Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// [`FromRef`]: https://docs.rs/axum/latest/axum/extract/trait.FromRef.html
|
||||
/// [`FromRef`]: https://docs.rs/axum/0.7/axum/extract/trait.FromRef.html
|
||||
#[proc_macro_derive(FromRef, attributes(from_ref))]
|
||||
pub fn derive_from_ref(item: TokenStream) -> TokenStream {
|
||||
expand_with(item, from_ref::expand)
|
||||
|
|
|
@ -40,10 +40,10 @@ impl<I> WithPosition<I>
|
|||
where
|
||||
I: Iterator,
|
||||
{
|
||||
pub(crate) fn new(iter: I) -> WithPosition<I> {
|
||||
pub(crate) fn new(iter: impl IntoIterator<IntoIter = I>) -> WithPosition<I> {
|
||||
WithPosition {
|
||||
handled_first: false,
|
||||
peekable: iter.fuse().peekable(),
|
||||
peekable: iter.into_iter().fuse().peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use axum_macros::debug_handler;
|
||||
|
||||
#[debug_handler]
|
||||
async fn handler(foo: bool) {}
|
||||
async fn handler(_foo: bool) {}
|
||||
|
||||
fn main() {}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied
|
||||
--> tests/debug_handler/fail/argument_not_extractor.rs:4:23
|
||||
error[E0277]: the trait bound `bool: FromRequest<(), axum_core::extract::private::ViaParts>` is not satisfied
|
||||
--> tests/debug_handler/fail/argument_not_extractor.rs:4:24
|
||||
|
|
||||
4 | async fn handler(foo: bool) {}
|
||||
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`
|
||||
4 | async fn handler(_foo: bool) {}
|
||||
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`, which is required by `bool: FromRequest<(), _>`
|
||||
|
|
||||
= note: Function argument is not a valid axum extractor.
|
||||
See `https://docs.rs/axum/latest/axum/extract/index.html` for details
|
||||
= help: the following other types implement trait `FromRequestParts<S>`:
|
||||
<() as FromRequestParts<S>>
|
||||
<(T1, T2) as FromRequestParts<S>>
|
||||
<(T1, T2, T3) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5, T6) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5, T6, T7) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5, T6, T7, T8) as FromRequestParts<S>>
|
||||
See `https://docs.rs/axum/0.7/axum/extract/index.html` for details
|
||||
= help: the following other types implement trait `FromRequest<S, M>`:
|
||||
axum::body::Bytes
|
||||
Body
|
||||
Form<T>
|
||||
Json<T>
|
||||
axum::http::Request<Body>
|
||||
RawForm
|
||||
String
|
||||
Option<T>
|
||||
and $N others
|
||||
= note: required for `bool` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
|
||||
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
|
||||
--> tests/debug_handler/fail/argument_not_extractor.rs:4:23
|
||||
--> tests/debug_handler/fail/argument_not_extractor.rs:4:24
|
||||
|
|
||||
4 | async fn handler(foo: bool) {}
|
||||
| ^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`
|
||||
4 | async fn handler(_foo: bool) {}
|
||||
| ^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
use axum::extract::Extension;
|
||||
use axum_macros::debug_handler;
|
||||
|
||||
struct NonCloneType;
|
||||
|
||||
#[debug_handler]
|
||||
async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,28 @@
|
|||
error[E0277]: the trait bound `NonCloneType: Clone` is not satisfied
|
||||
--> tests/debug_handler/fail/extension_not_clone.rs:7:38
|
||||
|
|
||||
7 | async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonCloneType`, which is required by `Extension<NonCloneType>: FromRequest<(), _>`
|
||||
|
|
||||
= help: the following other types implement trait `FromRequest<S, M>`:
|
||||
axum::body::Bytes
|
||||
Body
|
||||
Form<T>
|
||||
Json<T>
|
||||
axum::http::Request<Body>
|
||||
RawForm
|
||||
String
|
||||
Option<T>
|
||||
and $N others
|
||||
= note: required for `Extension<NonCloneType>` to implement `FromRequestParts<()>`
|
||||
= note: required for `Extension<NonCloneType>` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
|
||||
note: required by a bound in `__axum_macros_check_test_extension_non_clone_0_from_request_check`
|
||||
--> tests/debug_handler/fail/extension_not_clone.rs:7:38
|
||||
|
|
||||
7 | async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `__axum_macros_check_test_extension_non_clone_0_from_request_check`
|
||||
help: consider annotating `NonCloneType` with `#[derive(Clone)]`
|
||||
|
|
||||
4 + #[derive(Clone)]
|
||||
5 | struct NonCloneType;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use axum_macros::debug_handler;
|
||||
|
||||
#[debug_handler]
|
||||
async fn handler<T>(extract: T) {}
|
||||
async fn handler<T>(_extract: T) {}
|
||||
|
||||
fn main() {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
error: `#[axum_macros::debug_handler]` doesn't support generic functions
|
||||
--> tests/debug_handler/fail/generics.rs:4:17
|
||||
|
|
||||
4 | async fn handler<T>(extract: T) {}
|
||||
4 | async fn handler<T>(_extract: T) {}
|
||||
| ^^^
|
||||
|
|
|
@ -4,6 +4,6 @@ use axum_macros::debug_handler;
|
|||
struct Struct {}
|
||||
|
||||
#[debug_handler]
|
||||
async fn handler(foo: Json<Struct>) {}
|
||||
async fn handler(_foo: Json<Struct>) {}
|
||||
|
||||
fn main() {}
|
||||
|
|
|
@ -1,20 +1,44 @@
|
|||
error[E0277]: the trait bound `for<'de> Struct: serde::de::Deserialize<'de>` is not satisfied
|
||||
--> tests/debug_handler/fail/json_not_deserialize.rs:7:23
|
||||
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
||||
|
|
||||
7 | async fn handler(foo: Json<Struct>) {}
|
||||
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `Struct`
|
||||
7 | async fn handler(_foo: Json<Struct>) {}
|
||||
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
|
||||
|
|
||||
= help: the following other types implement trait `serde::de::Deserialize<'de>`:
|
||||
&'a [u8]
|
||||
&'a serde_json::raw::RawValue
|
||||
&'a std::path::Path
|
||||
&'a str
|
||||
()
|
||||
(T0, T1)
|
||||
(T0, T1, T2)
|
||||
(T0, T1, T2, T3)
|
||||
bool
|
||||
char
|
||||
isize
|
||||
i8
|
||||
i16
|
||||
i32
|
||||
i64
|
||||
i128
|
||||
and $N others
|
||||
= note: required for `Struct` to implement `serde::de::DeserializeOwned`
|
||||
= note: required for `Json<Struct>` to implement `FromRequest<()>`
|
||||
= help: see issue #48214
|
||||
= help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
||||
|
||||
error[E0277]: the trait bound `for<'de> Struct: serde::de::Deserialize<'de>` is not satisfied
|
||||
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
||||
|
|
||||
7 | async fn handler(_foo: Json<Struct>) {}
|
||||
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
|
||||
|
|
||||
= help: the following other types implement trait `serde::de::Deserialize<'de>`:
|
||||
bool
|
||||
char
|
||||
isize
|
||||
i8
|
||||
i16
|
||||
i32
|
||||
i64
|
||||
i128
|
||||
and $N others
|
||||
= note: required for `Struct` to implement `serde::de::DeserializeOwned`
|
||||
= note: required for `Json<Struct>` to implement `FromRequest<()>`
|
||||
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
|
||||
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
||||
|
|
||||
7 | async fn handler(_foo: Json<Struct>) {}
|
||||
| ^^^^^^^^^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`
|
||||
|
|
|
@ -2,7 +2,7 @@ use axum_macros::debug_handler;
|
|||
|
||||
#[debug_handler]
|
||||
async fn handler() {
|
||||
let rc = std::rc::Rc::new(());
|
||||
let _rc = std::rc::Rc::new(());
|
||||
async {}.await;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,16 +4,14 @@ error: future cannot be sent between threads safely
|
|||
3 | #[debug_handler]
|
||||
| ^^^^^^^^^^^^^^^^ future returned by `handler` is not `Send`
|
||||
|
|
||||
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`
|
||||
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`, which is required by `impl Future<Output = ()>: Send`
|
||||
note: future is not `Send` as this value is used across an await
|
||||
--> tests/debug_handler/fail/not_send.rs:6:13
|
||||
--> tests/debug_handler/fail/not_send.rs:6:14
|
||||
|
|
||||
5 | let rc = std::rc::Rc::new(());
|
||||
| -- has type `Rc<()>` which is not `Send`
|
||||
5 | let _rc = std::rc::Rc::new(());
|
||||
| --- has type `Rc<()>` which is not `Send`
|
||||
6 | async {}.await;
|
||||
| ^^^^^^ await occurs here, with `rc` maybe used later
|
||||
7 | }
|
||||
| - `rc` is later dropped here
|
||||
| ^^^^^ await occurs here, with `_rc` maybe used later
|
||||
note: required by a bound in `check`
|
||||
--> tests/debug_handler/fail/not_send.rs:3:1
|
||||
|
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
use axum::response::AppendHeaders;
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn handler(
|
||||
) -> (
|
||||
axum::http::StatusCode,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
axum::http::StatusCode,
|
||||
) {
|
||||
panic!()
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,12 @@
|
|||
error: Cannot return tuples with more than 17 elements
|
||||
--> tests/debug_handler/fail/output_tuple_too_many.rs:5:3
|
||||
|
|
||||
5 | ) -> (
|
||||
| ___^
|
||||
6 | | axum::http::StatusCode,
|
||||
7 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
8 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
|
||||
... |
|
||||
24 | | axum::http::StatusCode,
|
||||
25 | | ) {
|
||||
| |_^
|
|
@ -0,0 +1,10 @@
|
|||
#[axum::debug_handler]
|
||||
async fn handler(
|
||||
) -> (
|
||||
axum::http::request::Parts, // this should be response parts, not request parts
|
||||
axum::http::StatusCode,
|
||||
) {
|
||||
panic!()
|
||||
}
|
||||
|
||||
fn main(){}
|
|
@ -0,0 +1,8 @@
|
|||
error[E0308]: mismatched types
|
||||
--> tests/debug_handler/fail/returning_request_parts.rs:4:5
|
||||
|
|
||||
4 | axum::http::request::Parts, // this should be response parts, not request parts
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| |
|
||||
| expected `axum::http::response::Parts`, found `axum::http::request::Parts`
|
||||
| expected `axum::http::response::Parts` because of return type
|
|
@ -0,0 +1,10 @@
|
|||
#![allow(unused_parens)]
|
||||
|
||||
struct NotIntoResponse;
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn handler() -> (NotIntoResponse) {
|
||||
panic!()
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,21 @@
|
|||
error[E0277]: the trait bound `NotIntoResponse: IntoResponse` is not satisfied
|
||||
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
||||
|
|
||||
6 | async fn handler() -> (NotIntoResponse) {
|
||||
| ^^^^^^^^^^^^^^^^^ the trait `IntoResponse` is not implemented for `NotIntoResponse`
|
||||
|
|
||||
= help: the following other types implement trait `IntoResponse`:
|
||||
Box<str>
|
||||
Box<[u8]>
|
||||
axum::body::Bytes
|
||||
Body
|
||||
axum::extract::rejection::FailedToBufferBody
|
||||
axum::extract::rejection::LengthLimitError
|
||||
axum::extract::rejection::UnknownBodyError
|
||||
axum::extract::rejection::InvalidUtf8
|
||||
and $N others
|
||||
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
|
||||
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
||||
|
|
||||
6 | async fn handler() -> (NotIntoResponse) {
|
||||
| ^^^^^^^^^^^^^^^^^ required by this bound in `check`
|
|
@ -3,23 +3,23 @@ use axum::http::Uri;
|
|||
|
||||
#[debug_handler]
|
||||
async fn handler(
|
||||
e1: Uri,
|
||||
e2: Uri,
|
||||
e3: Uri,
|
||||
e4: Uri,
|
||||
e5: Uri,
|
||||
e6: Uri,
|
||||
e7: Uri,
|
||||
e8: Uri,
|
||||
e9: Uri,
|
||||
e10: Uri,
|
||||
e11: Uri,
|
||||
e12: Uri,
|
||||
e13: Uri,
|
||||
e14: Uri,
|
||||
e15: Uri,
|
||||
e16: Uri,
|
||||
e17: Uri,
|
||||
_e1: Uri,
|
||||
_e2: Uri,
|
||||
_e3: Uri,
|
||||
_e4: Uri,
|
||||
_e5: Uri,
|
||||
_e6: Uri,
|
||||
_e7: Uri,
|
||||
_e8: Uri,
|
||||
_e9: Uri,
|
||||
_e10: Uri,
|
||||
_e11: Uri,
|
||||
_e12: Uri,
|
||||
_e13: Uri,
|
||||
_e14: Uri,
|
||||
_e15: Uri,
|
||||
_e16: Uri,
|
||||
_e17: Uri,
|
||||
) {}
|
||||
|
||||
fn main() {}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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,
|
||||
| |_____________^
|
||||
21 | | _e16: Uri,
|
||||
22 | | _e17: Uri,
|
||||
| |______________^
|
||||
|
|
30
axum-macros/tests/debug_handler/fail/wrong_return_tuple.rs
Normal file
30
axum-macros/tests/debug_handler/fail/wrong_return_tuple.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
#![allow(unused_parens)]
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn named_type() -> (
|
||||
axum::http::StatusCode,
|
||||
axum::Json<&'static str>,
|
||||
axum::response::AppendHeaders<[( axum::http::HeaderName,&'static str); 1]>,
|
||||
) {
|
||||
panic!()
|
||||
}
|
||||
|
||||
|
||||
struct CustomIntoResponse{
|
||||
|
||||
}
|
||||
impl axum::response::IntoResponse for CustomIntoResponse{
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
#[axum::debug_handler]
|
||||
async fn custom_type() -> (
|
||||
axum::http::StatusCode,
|
||||
CustomIntoResponse,
|
||||
axum::response::AppendHeaders<[( axum::http::HeaderName,&'static str); 1]>,
|
||||
) {
|
||||
panic!()
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,46 @@
|
|||
error: `Json<_>` must be the last element in a response tuple
|
||||
--> tests/debug_handler/fail/wrong_return_tuple.rs:6:5
|
||||
|
|
||||
6 | axum::Json<&'static str>,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not satisfied
|
||||
--> tests/debug_handler/fail/wrong_return_tuple.rs:24:5
|
||||
|
|
||||
24 | CustomIntoResponse,
|
||||
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
||||
|
|
||||
= help: the following other types implement trait `IntoResponseParts`:
|
||||
AppendHeaders<I>
|
||||
HeaderMap
|
||||
Extension<T>
|
||||
Extensions
|
||||
Option<T>
|
||||
[(K, V); N]
|
||||
()
|
||||
(T1,)
|
||||
and $N others
|
||||
= help: see issue #48214
|
||||
= help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
||||
|
||||
error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not satisfied
|
||||
--> tests/debug_handler/fail/wrong_return_tuple.rs:24:5
|
||||
|
|
||||
24 | CustomIntoResponse,
|
||||
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
||||
|
|
||||
= help: the following other types implement trait `IntoResponseParts`:
|
||||
AppendHeaders<I>
|
||||
HeaderMap
|
||||
Extension<T>
|
||||
Extensions
|
||||
Option<T>
|
||||
[(K, V); N]
|
||||
()
|
||||
(T1,)
|
||||
and $N others
|
||||
note: required by a bound in `__axum_macros_check_custom_type_into_response_parts_1_check`
|
||||
--> tests/debug_handler/fail/wrong_return_tuple.rs:24:5
|
||||
|
|
||||
24 | CustomIntoResponse,
|
||||
| ^^^^^^^^^^^^^^^^^^ required by this bound in `__axum_macros_check_custom_type_into_response_parts_1_check`
|
|
@ -5,14 +5,14 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
|
|||
| ^^^^ the trait `IntoResponse` is not implemented for `bool`
|
||||
|
|
||||
= help: the following other types implement trait `IntoResponse`:
|
||||
&'static [u8; N]
|
||||
&'static [u8]
|
||||
&'static str
|
||||
()
|
||||
(R,)
|
||||
(Response<()>, R)
|
||||
(Response<()>, T1, R)
|
||||
(Response<()>, T1, T2, R)
|
||||
Box<str>
|
||||
Box<[u8]>
|
||||
axum::body::Bytes
|
||||
Body
|
||||
axum::extract::rejection::FailedToBufferBody
|
||||
axum::extract::rejection::LengthLimitError
|
||||
axum::extract::rejection::UnknownBodyError
|
||||
axum::extract::rejection::InvalidUtf8
|
||||
and $N others
|
||||
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
|
||||
--> tests/debug_handler/fail/wrong_return_type.rs:4:23
|
||||
|
|
13
axum-macros/tests/debug_middleware/fail/doesnt_take_next.rs
Normal file
13
axum-macros/tests/debug_middleware/fail/doesnt_take_next.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use axum::{
|
||||
debug_middleware,
|
||||
extract::Request,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(request: Request) -> Response {
|
||||
let _ = request;
|
||||
().into_response()
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,7 @@
|
|||
error: Middleware functions must take `axum::middleware::Next` as the last argument
|
||||
--> tests/debug_middleware/fail/doesnt_take_next.rs:7:1
|
||||
|
|
||||
7 | #[debug_middleware]
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the attribute macro `debug_middleware` (in Nightly builds, run with -Z macro-backtrace for more info)
|
13
axum-macros/tests/debug_middleware/fail/next_not_last.rs
Normal file
13
axum-macros/tests/debug_middleware/fail/next_not_last.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use axum::{
|
||||
extract::Request,
|
||||
response::Response,
|
||||
middleware::Next,
|
||||
debug_middleware,
|
||||
};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(next: Next, request: Request) -> Response {
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,5 @@
|
|||
error: `axum::middleware::Next` must the last argument
|
||||
--> tests/debug_middleware/fail/next_not_last.rs:9:24
|
||||
|
|
||||
9 | async fn my_middleware(next: Next, request: Request) -> Response {
|
||||
| ^^^^^^^^^^
|
|
@ -0,0 +1,9 @@
|
|||
use axum::{debug_middleware, extract::Request, middleware::Next, response::Response};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(request: Request, next: Next, next2: Next) -> Response {
|
||||
let _ = next2;
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,7 @@
|
|||
error: Middleware functions can only take one argument of type `axum::middleware::Next`
|
||||
--> tests/debug_middleware/fail/takes_next_twice.rs:3:1
|
||||
|
|
||||
3 | #[debug_middleware]
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the attribute macro `debug_middleware` (in Nightly builds, run with -Z macro-backtrace for more info)
|
13
axum-macros/tests/debug_middleware/pass/basic.rs
Normal file
13
axum-macros/tests/debug_middleware/pass/basic.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use axum::{
|
||||
extract::Request,
|
||||
response::Response,
|
||||
middleware::Next,
|
||||
debug_middleware,
|
||||
};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(request: Request, next: Next) -> Response {
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -20,5 +20,8 @@ note: required by a bound in `axum::routing::get`
|
|||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
| top_level_handler_fn!(get, GET);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `get`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
|
||||
| | |
|
||||
| | required by a bound in this function
|
||||
| required by this bound in `get`
|
||||
= note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
|
|
@ -20,5 +20,8 @@ note: required by a bound in `axum::routing::get`
|
|||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
| top_level_handler_fn!(get, GET);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `get`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
|
||||
| | |
|
||||
| | required by a bound in this function
|
||||
| required by this bound in `get`
|
||||
= note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
|
|
@ -20,7 +20,10 @@ note: required by a bound in `axum::routing::get`
|
|||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
| top_level_handler_fn!(get, GET);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `get`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
|
||||
| | |
|
||||
| | required by a bound in this function
|
||||
| required by this bound in `get`
|
||||
= note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0277]: the trait bound `fn(Result<MyExtractor, MyRejection>) -> impl Future<Output = ()> {handler_result}: Handler<_, _>` is not satisfied
|
||||
|
@ -39,5 +42,8 @@ note: required by a bound in `MethodRouter::<S>::post`
|
|||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
| chained_handler_fn!(post, POST);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `MethodRouter::<S>::post`
|
||||
| ^^^^^^^^^^^^^^^^^^^^----^^^^^^^
|
||||
| | |
|
||||
| | required by a bound in this associated function
|
||||
| required by this bound in `MethodRouter::<S>::post`
|
||||
= note: this error originates in the macro `chained_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
|
|
@ -5,14 +5,14 @@ error[E0277]: the trait bound `String: FromRequestParts<S>` is not satisfied
|
|||
| ^^^^^^ the trait `FromRequestParts<S>` is not implemented for `String`
|
||||
|
|
||||
= note: Function argument is not a valid axum extractor.
|
||||
See `https://docs.rs/axum/latest/axum/extract/index.html` for details
|
||||
See `https://docs.rs/axum/0.7/axum/extract/index.html` for details
|
||||
= help: the following other types implement trait `FromRequestParts<S>`:
|
||||
<() as FromRequestParts<S>>
|
||||
<(T1, T2) as FromRequestParts<S>>
|
||||
<(T1, T2, T3) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5, T6) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5, T6, T7) as FromRequestParts<S>>
|
||||
<(T1, T2, T3, T4, T5, T6, T7, T8) as FromRequestParts<S>>
|
||||
<Extractor as FromRequestParts<S>>
|
||||
<HeaderMap as FromRequestParts<S>>
|
||||
<Extension<T> as FromRequestParts<S>>
|
||||
<Method as FromRequestParts<S>>
|
||||
<axum::http::request::Parts as FromRequestParts<S>>
|
||||
<Uri as FromRequestParts<S>>
|
||||
<Version as FromRequestParts<S>>
|
||||
<Extensions as FromRequestParts<S>>
|
||||
and $N others
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
error[E0277]: the trait bound `for<'de> MyPath: serde::de::Deserialize<'de>` is not satisfied
|
||||
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::de::Deserialize<'de>` is not implemented for `MyPath`
|
||||
| ^^^^^^^^^ the trait `for<'de> serde::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::de::Deserialize<'de>`:
|
||||
&'a [u8]
|
||||
&'a serde_json::raw::RawValue
|
||||
&'a std::path::Path
|
||||
&'a str
|
||||
()
|
||||
(T0, T1)
|
||||
(T0, T1, T2)
|
||||
(T0, T1, T2, T3)
|
||||
and $N others
|
||||
= help: the trait `FromRequestParts<S>` is implemented for `axum::extract::Path<T>`
|
||||
= note: required for `MyPath` to implement `serde::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::de::DeserializeOwned` is not satisfied
|
||||
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
||||
|
|
||||
3 | #[derive(TypedPath)]
|
||||
| ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `MyPath`, which is required by `axum::extract::Path<MyPath>: FromRequestParts<S>`
|
||||
|
|
||||
= help: the trait `FromRequestParts<S>` is implemented for `axum::extract::Path<T>`
|
||||
= note: required for `MyPath` to implement `serde::de::DeserializeOwned`
|
||||
= note: required for `axum::extract::Path<MyPath>` to implement `FromRequestParts<S>`
|
||||
= note: this error originates in the attribute macro `::axum::async_trait` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
|
|
@ -7,6 +7,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
# Unreleased
|
||||
|
||||
- **change:** Avoid cloning `Arc` during deserialization of `Path`
|
||||
- **added:** `axum::serve::Serve::tcp_nodelay` and `axum::serve::WithGracefulShutdown::tcp_nodelay` ([#2653])
|
||||
|
||||
[#2653]: https://github.com/tokio-rs/axum/pull/2653
|
||||
|
||||
# 0.7.5 (24. March, 2024)
|
||||
|
||||
- **fixed:** Fixed layers being cloned when calling `axum::serve` directly with
|
||||
a `Router` or `MethodRouter` ([#2586])
|
||||
- **fixed:** `h2` is no longer pulled as a dependency unless the `http2` feature
|
||||
is enabled ([#2605])
|
||||
- **added:** Add `#[debug_middleware]` ([#1993], [#2725])
|
||||
|
||||
[#1993]: https://github.com/tokio-rs/axum/pull/1993
|
||||
[#2725]: https://github.com/tokio-rs/axum/pull/2725
|
||||
[#2586]: https://github.com/tokio-rs/axum/pull/2586
|
||||
[#2605]: https://github.com/tokio-rs/axum/pull/2605
|
||||
|
||||
# 0.7.4 (13. January, 2024)
|
||||
|
||||
- **fixed:** Fix performance regression present since axum 0.7.0 ([#2483])
|
||||
- **fixed:** Improve `debug_handler` on tuple response types ([#2201])
|
||||
- **added:** Add `must_use` attribute to `Serve` and `WithGracefulShutdown` ([#2484])
|
||||
- **added:** Re-export `axum_core::body::BodyDataStream` from axum
|
||||
|
||||
[#2201]: https://github.com/tokio-rs/axum/pull/2201
|
||||
[#2483]: https://github.com/tokio-rs/axum/pull/2483
|
||||
[#2201]: https://github.com/tokio-rs/axum/pull/2201
|
||||
[#2484]: https://github.com/tokio-rs/axum/pull/2484
|
||||
|
||||
# 0.7.3 (29. December, 2023)
|
||||
|
||||
- **added:** `Body` implements `From<()>` now ([#2411])
|
||||
- **change:** Update version of multer used internally for multipart ([#2433])
|
||||
- **change:** Update tokio-tungstenite to 0.21 ([#2435])
|
||||
- **added:** Enable `tracing` feature by default ([#2460])
|
||||
- **added:** Support graceful shutdown on `serve` ([#2398])
|
||||
- **added:** `RouterIntoService` implements `Clone` ([#2456])
|
||||
|
||||
[#2411]: https://github.com/tokio-rs/axum/pull/2411
|
||||
[#2433]: https://github.com/tokio-rs/axum/pull/2433
|
||||
[#2435]: https://github.com/tokio-rs/axum/pull/2435
|
||||
[#2460]: https://github.com/tokio-rs/axum/pull/2460
|
||||
[#2398]: https://github.com/tokio-rs/axum/pull/2398
|
||||
[#2456]: https://github.com/tokio-rs/axum/pull/2456
|
||||
|
||||
# 0.7.2 (03. December, 2023)
|
||||
|
||||
- **added:** Add `axum::body::to_bytes` ([#2373])
|
||||
- **fixed:** Gracefully handle accept errors in `serve` ([#2400])
|
||||
|
||||
[#2373]: https://github.com/tokio-rs/axum/pull/2373
|
||||
[#2400]: https://github.com/tokio-rs/axum/pull/2400
|
||||
|
||||
# 0.7.1 (27. November, 2023)
|
||||
|
||||
- **fix**: Fix readme.
|
||||
|
||||
# 0.7.0 (27. November, 2023)
|
||||
|
||||
- **breaking:** Update public dependencies. axum now requires
|
||||
- [hyper](https://crates.io/crates/hyper) 1.0
|
||||
- [http](https://crates.io/crates/http) 1.0
|
||||
- [http-body](https://crates.io/crates/http-body) 1.0
|
||||
- **breaking:** axum now requires [tower-http](https://crates.io/crates/tower-http) 0.5
|
||||
- **breaking:** Remove deprecated `WebSocketUpgrade::max_send_queue`
|
||||
- **breaking:** The following types/traits are no longer generic over the request body
|
||||
(i.e. the `B` type param has been removed) ([#1751] and [#1789]):
|
||||
|
@ -30,7 +95,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- **breaking:** Change `sse::Event::json_data` to use `axum_core::Error` as its error type ([#1762])
|
||||
- **breaking:** Rename `DefaultOnFailedUpdgrade` to `DefaultOnFailedUpgrade` ([#1664])
|
||||
- **breaking:** Rename `OnFailedUpdgrade` to `OnFailedUpgrade` ([#1664])
|
||||
- **breaking:** `TypedHeader` has been move to `axum-extra` ([#1850])
|
||||
- **breaking:** `TypedHeader` has been moved to `axum-extra` as `axum_extra::TypedHeader` and requires enabling the `typed-header` feature on `axum-extra`. The `headers` feature has been removed from axum; what it provided under `axum::headers` is now found in `axum_extra::headers` by default. ([#1850])
|
||||
- **breaking:** Removed re-exports of `Empty` and `Full`. Use
|
||||
`axum::body::Body::empty` and `axum::body::Body::from` respectively ([#1789])
|
||||
- **breaking:** The response returned by `IntoResponse::into_response` must use
|
||||
|
@ -60,14 +125,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- **fixed:** Fix bugs around merging routers with nested fallbacks ([#2096])
|
||||
- **fixed:** Fix `.source()` of composite rejections ([#2030])
|
||||
- **fixed:** Allow unreachable code in `#[debug_handler]` ([#2014])
|
||||
- **change:** Update tokio-tungstenite to 0.19 ([#2021])
|
||||
- **change:** axum's MSRV is now 1.63 ([#2021])
|
||||
- **added:** Implement `Handler` for `T: IntoResponse` ([#2140])
|
||||
- **change:** axum's MSRV is now 1.66 ([#1882])
|
||||
- **added:** Implement `IntoResponse` for `(R,) where R: IntoResponse` ([#2143])
|
||||
- **changed:** For SSE, add space between field and value for compatibility ([#2149])
|
||||
- **added:** Add `NestedPath` extractor ([#1924])
|
||||
- **added:** Add `handle_error` function to existing `ServiceExt` trait ([#2235])
|
||||
- **breaking:** `impl<T> IntoResponse(Parts) for Extension<T>` now requires
|
||||
`T: Clone`, as that is required by the http crate ([#1882])
|
||||
- **added:** Add `axum::Json::from_bytes` ([#2244])
|
||||
- **added:** Implement `FromRequestParts` for `http::request::Parts` ([#2328])
|
||||
- **added:** Implement `FromRequestParts` for `http::Extensions` ([#2328])
|
||||
- **fixed:** Clearly document applying `DefaultBodyLimit` to individual routes ([#2157])
|
||||
|
||||
[#2021]: https://github.com/tokio-rs/axum/pull/2021
|
||||
[#2014]: https://github.com/tokio-rs/axum/pull/2014
|
||||
[#2030]: https://github.com/tokio-rs/axum/pull/2030
|
||||
[#1664]: https://github.com/tokio-rs/axum/pull/1664
|
||||
[#1751]: https://github.com/tokio-rs/axum/pull/1751
|
||||
[#1762]: https://github.com/tokio-rs/axum/pull/1762
|
||||
|
@ -75,13 +144,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#1835]: https://github.com/tokio-rs/axum/pull/1835
|
||||
[#1850]: https://github.com/tokio-rs/axum/pull/1850
|
||||
[#1868]: https://github.com/tokio-rs/axum/pull/1868
|
||||
[#1882]: https://github.com/tokio-rs/axum/pull/1882
|
||||
[#1924]: https://github.com/tokio-rs/axum/pull/1924
|
||||
[#1956]: https://github.com/tokio-rs/axum/pull/1956
|
||||
[#1972]: https://github.com/tokio-rs/axum/pull/1972
|
||||
[#2014]: https://github.com/tokio-rs/axum/pull/2014
|
||||
[#2021]: https://github.com/tokio-rs/axum/pull/2021
|
||||
[#2030]: https://github.com/tokio-rs/axum/pull/2030
|
||||
[#2058]: https://github.com/tokio-rs/axum/pull/2058
|
||||
[#2073]: https://github.com/tokio-rs/axum/pull/2073
|
||||
[#2096]: https://github.com/tokio-rs/axum/pull/2096
|
||||
[#2140]: https://github.com/tokio-rs/axum/pull/2140
|
||||
[#2143]: https://github.com/tokio-rs/axum/pull/2143
|
||||
[#2149]: https://github.com/tokio-rs/axum/pull/2149
|
||||
[#2157]: https://github.com/tokio-rs/axum/pull/2157
|
||||
[#2235]: https://github.com/tokio-rs/axum/pull/2235
|
||||
[#2244]: https://github.com/tokio-rs/axum/pull/2244
|
||||
[#2328]: https://github.com/tokio-rs/axum/pull/2328
|
||||
|
||||
# 0.6.20 (03. August, 2023)
|
||||
|
||||
- **added:** `WebSocketUpgrade::write_buffer_size` and `WebSocketUpgrade::max_write_buffer_size`
|
||||
- **changed:** Deprecate `WebSocketUpgrade::max_send_queue`
|
||||
- **change:** Update tokio-tungstenite to 0.20
|
||||
- **added:** Implement `Handler` for `T: IntoResponse` ([#2140])
|
||||
|
||||
[#2140]: https://github.com/tokio-rs/axum/pull/2140
|
||||
|
||||
# 0.6.19 (17. July, 2023)
|
||||
|
||||
- **added:** Add `axum::extract::Query::try_from_uri` ([#2058])
|
||||
- **added:** Implement `IntoResponse` for `Box<str>` and `Box<[u8]>` ([#2035])
|
||||
- **fixed:** Fix bugs around merging routers with nested fallbacks ([#2096])
|
||||
- **fixed:** Fix `.source()` of composite rejections ([#2030])
|
||||
- **fixed:** Allow unreachable code in `#[debug_handler]` ([#2014])
|
||||
- **change:** Update tokio-tungstenite to 0.19 ([#2021])
|
||||
- **change:** axum's MSRV is now 1.63 ([#2021])
|
||||
|
||||
[#2014]: https://github.com/tokio-rs/axum/pull/2014
|
||||
[#2021]: https://github.com/tokio-rs/axum/pull/2021
|
||||
[#2030]: https://github.com/tokio-rs/axum/pull/2030
|
||||
[#2035]: https://github.com/tokio-rs/axum/pull/2035
|
||||
[#2058]: https://github.com/tokio-rs/axum/pull/2058
|
||||
[#2096]: https://github.com/tokio-rs/axum/pull/2096
|
||||
|
||||
# 0.6.18 (30. April, 2023)
|
||||
|
||||
- **fixed:** Don't remove the `Sec-WebSocket-Key` header in `WebSocketUpgrade` ([#1972])
|
||||
|
||||
[#1972]: https://github.com/tokio-rs/axum/pull/1972
|
||||
|
||||
# 0.6.17 (25. April, 2023)
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "axum"
|
||||
version = "0.6.16"
|
||||
version = "0.7.5"
|
||||
categories = ["asynchronous", "network-programming", "web-programming::http-server"]
|
||||
description = "Web framework that focuses on ergonomics and modularity"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
rust-version = "1.66"
|
||||
homepage = "https://github.com/tokio-rs/axum"
|
||||
keywords = ["http", "web", "framework"]
|
||||
license = "MIT"
|
||||
|
@ -12,62 +12,71 @@ readme = "README.md"
|
|||
repository = "https://github.com/tokio-rs/axum"
|
||||
|
||||
[features]
|
||||
default = ["form", "http1", "json", "matched-path", "original-uri", "query", "tokio", "tower-log"]
|
||||
default = [
|
||||
"form",
|
||||
"http1",
|
||||
"json",
|
||||
"matched-path",
|
||||
"original-uri",
|
||||
"query",
|
||||
"tokio",
|
||||
"tower-log",
|
||||
"tracing",
|
||||
]
|
||||
form = ["dep:serde_urlencoded"]
|
||||
http1 = ["hyper/http1"]
|
||||
http2 = ["hyper/http2"]
|
||||
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"]
|
||||
macros = ["dep:axum-macros"]
|
||||
matched-path = []
|
||||
multipart = ["dep:multer"]
|
||||
original-uri = []
|
||||
query = ["dep:serde_urlencoded"]
|
||||
tokio = ["dep:tokio", "hyper/server", "hyper/tcp", "hyper/runtime", "tower/make"]
|
||||
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 = ["tokio", "dep:tokio-tungstenite", "dep:sha1", "dep:base64"]
|
||||
ws = ["dep:hyper", "tokio", "dep:tokio-tungstenite", "dep:sha1", "dep:base64"]
|
||||
|
||||
# Required for intra-doc links to resolve correctly
|
||||
__private_docs = ["tower/full", "dep:tower-http"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.67"
|
||||
axum-core = { path = "../axum-core", version = "0.3.4" }
|
||||
axum-core = { path = "../axum-core", version = "0.4.3" }
|
||||
bytes = "1.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.9"
|
||||
http-body = "0.4.4"
|
||||
hyper = { version = "0.14.24", features = ["stream"] }
|
||||
http = "1.0.0"
|
||||
http-body = "1.0.0"
|
||||
http-body-util = "0.1.0"
|
||||
itoa = "1.0.5"
|
||||
matchit = "0.7"
|
||||
memchr = "2.4.1"
|
||||
mime = "0.3.16"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
rustversion = "1.0.9"
|
||||
serde = "1.0"
|
||||
sync_wrapper = "0.1.1"
|
||||
sync_wrapper = "1.0.0"
|
||||
tower = { version = "0.4.13", default-features = false, features = ["util"] }
|
||||
tower-layer = "0.3.2"
|
||||
tower-service = "0.3"
|
||||
|
||||
# wont need this when axum uses http-body 1.0
|
||||
hyper1 = { package = "hyper", version = "=1.0.0-rc.4", features = ["server", "http1"] }
|
||||
tower-hyper-http-body-compat = { version = "0.2", features = ["server", "http1"] }
|
||||
|
||||
# optional dependencies
|
||||
axum-macros = { path = "../axum-macros", version = "0.3.7", optional = true }
|
||||
axum-macros = { path = "../axum-macros", version = "0.4.1", optional = true }
|
||||
base64 = { version = "0.21.0", optional = true }
|
||||
multer = { version = "2.0.0", optional = true }
|
||||
hyper = { version = "1.1.0", optional = true }
|
||||
hyper-util = { version = "0.1.3", features = ["tokio", "server", "service"], optional = true }
|
||||
multer = { version = "3.0.0", optional = true }
|
||||
serde_json = { version = "1.0", 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.25.0", features = ["time"], optional = true }
|
||||
tokio-tungstenite = { version = "0.20", optional = true }
|
||||
tokio-tungstenite = { version = "0.21", optional = true }
|
||||
tracing = { version = "0.1", default-features = false, optional = true }
|
||||
|
||||
[dependencies.tower-http]
|
||||
version = "0.4"
|
||||
version = "0.5.0"
|
||||
optional = true
|
||||
features = [
|
||||
# all tower-http features except (de)?compression-zstd which doesn't
|
||||
|
@ -101,21 +110,18 @@ features = [
|
|||
"validate-request",
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
rustversion = "1.0.9"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0"
|
||||
axum-macros = { path = "../axum-macros", version = "0.3.7", features = ["__private"] }
|
||||
axum-macros = { path = "../axum-macros", version = "0.4.1", features = ["__private"] }
|
||||
quickcheck = "1.0"
|
||||
quickcheck_macros = "1.0"
|
||||
reqwest = { version = "0.11.14", default-features = false, features = ["json", "stream", "multipart"] }
|
||||
rustversion = "1.0.9"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
time = { version = "0.3", features = ["serde-human-readable"] }
|
||||
tokio = { package = "tokio", version = "1.25.0", features = ["macros", "rt", "rt-multi-thread", "net", "test-util"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-tungstenite = "0.21"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["json"] }
|
||||
uuid = { version = "1.0", features = ["serde", "v4"] }
|
||||
|
@ -137,7 +143,7 @@ features = [
|
|||
]
|
||||
|
||||
[dev-dependencies.tower-http]
|
||||
version = "0.4"
|
||||
version = "0.5.0"
|
||||
features = [
|
||||
# all tower-http features except (de)?compression-zstd which doesn't
|
||||
# build on `--target armv5te-unknown-linux-musleabi`
|
||||
|
@ -181,19 +187,24 @@ features = [
|
|||
|
||||
[package.metadata.cargo-public-api-crates]
|
||||
allowed = [
|
||||
"async_trait",
|
||||
# our crates
|
||||
"axum_core",
|
||||
"axum_macros",
|
||||
"bytes",
|
||||
|
||||
# not 1.0
|
||||
"futures_core",
|
||||
"futures_sink",
|
||||
"futures_util",
|
||||
"tower_layer",
|
||||
"tower_service",
|
||||
|
||||
# >=1.0
|
||||
"async_trait",
|
||||
"bytes",
|
||||
"http",
|
||||
"http_body",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tower_layer",
|
||||
"tower_service",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
|
|
|
@ -8,12 +8,6 @@
|
|||
|
||||
More information about this crate can be found in the [crate documentation][docs].
|
||||
|
||||
## 🚨 The `main` branch has unpublished, breaking changes 🚨
|
||||
|
||||
In preparation for `axum` 0.7 the `main` branch currently has unpublished,
|
||||
breaking changes. Please see the [v0.6.x](https://github.com/tokio-rs/axum/tree/v0.6.x)
|
||||
branch for the versions of `axum` published to crates.io.
|
||||
|
||||
## High level features
|
||||
|
||||
- Route requests to handlers with a macro free API.
|
||||
|
@ -35,11 +29,9 @@ applications written using [`hyper`] or [`tonic`].
|
|||
use axum::{
|
||||
routing::{get, post},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
@ -112,7 +104,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
|
|||
|
||||
## Minimum supported Rust version
|
||||
|
||||
axum's MSRV is 1.63.
|
||||
axum's MSRV is 1.66.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ use axum::{
|
|||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use hyper::server::conn::AddrIncoming;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
future::IntoFuture,
|
||||
io::BufRead,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
@ -162,13 +162,8 @@ impl BenchmarkBuilder {
|
|||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
rt.block_on(async move {
|
||||
let incoming = AddrIncoming::from_listener(listener).unwrap();
|
||||
hyper::Server::builder(incoming)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
rt.block_on(axum::serve(listener, app).into_future())
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let mut cmd = Command::new("rewrk");
|
||||
|
@ -203,7 +198,7 @@ impl BenchmarkBuilder {
|
|||
|
||||
eprintln!("Running {:?} benchmark", self.name);
|
||||
|
||||
// indent output from `rewrk` so its easier to read when running multiple benchmarks
|
||||
// indent output from `rewrk` so it's easier to read when running multiple benchmarks
|
||||
let mut child = cmd.spawn().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stdout = std::io::BufReader::new(stdout);
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#[rustversion::nightly]
|
||||
fn main() {
|
||||
println!("cargo:rustc-cfg=nightly_error_messages");
|
||||
}
|
||||
|
||||
#[rustversion::not(nightly)]
|
||||
fn main() {}
|
3
axum/clippy.toml
Normal file
3
axum/clippy.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
disallowed-types = [
|
||||
{ path = "std::sync::Mutex", reason = "Use our internal AxumMutex instead" },
|
||||
]
|
|
@ -7,4 +7,48 @@ pub use http_body::Body as HttpBody;
|
|||
pub use bytes::Bytes;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use axum_core::body::Body;
|
||||
pub use axum_core::body::{Body, BodyDataStream};
|
||||
|
||||
use http_body_util::{BodyExt, Limited};
|
||||
|
||||
/// Converts [`Body`] into [`Bytes`] and limits the maximum size of the body.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use axum::body::{to_bytes, Body};
|
||||
///
|
||||
/// # async fn foo() -> Result<(), axum_core::Error> {
|
||||
/// let body = Body::from(vec![1, 2, 3]);
|
||||
/// // Use `usize::MAX` if you don't care about the maximum size.
|
||||
/// let bytes = to_bytes(body, usize::MAX).await?;
|
||||
/// assert_eq!(&bytes[..], &[1, 2, 3]);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can detect if the limit was hit by checking the source of the error:
|
||||
///
|
||||
/// ```rust
|
||||
/// use axum::body::{to_bytes, Body};
|
||||
/// use http_body_util::LengthLimitError;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let body = Body::from(vec![1, 2, 3]);
|
||||
/// match to_bytes(body, 1).await {
|
||||
/// Ok(_bytes) => panic!("should have hit the limit"),
|
||||
/// Err(err) => {
|
||||
/// let source = std::error::Error::source(&err).unwrap();
|
||||
/// assert!(source.is::<LengthLimitError>());
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn to_bytes(body: Body, limit: usize) -> Result<Bytes, axum_core::Error> {
|
||||
Limited::new(body, limit)
|
||||
.collect()
|
||||
.await
|
||||
.map(|col| col.to_bytes())
|
||||
.map_err(axum_core::Error::new)
|
||||
}
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
use crate::{
|
||||
body::{Bytes, HttpBody},
|
||||
response::{IntoResponse, Response},
|
||||
BoxError, Error,
|
||||
};
|
||||
use axum_core::body::Body;
|
||||
use futures_util::{
|
||||
ready,
|
||||
stream::{self, TryStream},
|
||||
};
|
||||
use http::HeaderMap;
|
||||
use pin_project_lite::pin_project;
|
||||
use std::{
|
||||
fmt,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use sync_wrapper::SyncWrapper;
|
||||
|
||||
pin_project! {
|
||||
/// An [`http_body::Body`] created from a [`Stream`].
|
||||
///
|
||||
/// The purpose of this type is to be used in responses. If you want to
|
||||
/// extract the request body as a stream consider using
|
||||
/// [`Body`](crate::body::Body).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use axum::{
|
||||
/// Router,
|
||||
/// routing::get,
|
||||
/// body::StreamBody,
|
||||
/// response::IntoResponse,
|
||||
/// };
|
||||
/// use futures_util::stream::{self, Stream};
|
||||
/// use std::io;
|
||||
///
|
||||
/// async fn handler() -> StreamBody<impl Stream<Item = io::Result<&'static str>>> {
|
||||
/// let chunks: Vec<io::Result<_>> = vec![
|
||||
/// Ok("Hello,"),
|
||||
/// Ok(" "),
|
||||
/// Ok("world!"),
|
||||
/// ];
|
||||
/// let stream = stream::iter(chunks);
|
||||
/// StreamBody::new(stream)
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/", get(handler));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// [`Stream`]: futures_util::stream::Stream
|
||||
#[must_use]
|
||||
pub struct StreamBody<S> {
|
||||
#[pin]
|
||||
stream: SyncWrapper<S>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<S> for StreamBody<S>
|
||||
where
|
||||
S: TryStream + Send + 'static,
|
||||
S::Ok: Into<Bytes>,
|
||||
S::Error: Into<BoxError>,
|
||||
{
|
||||
fn from(stream: S) -> Self {
|
||||
Self::new(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> StreamBody<S> {
|
||||
/// Create a new `StreamBody` from a [`Stream`].
|
||||
///
|
||||
/// [`Stream`]: futures_util::stream::Stream
|
||||
pub fn new(stream: S) -> Self
|
||||
where
|
||||
S: TryStream + Send + 'static,
|
||||
S::Ok: Into<Bytes>,
|
||||
S::Error: Into<BoxError>,
|
||||
{
|
||||
Self {
|
||||
stream: SyncWrapper::new(stream),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> IntoResponse for StreamBody<S>
|
||||
where
|
||||
S: TryStream + Send + 'static,
|
||||
S::Ok: Into<Bytes>,
|
||||
S::Error: Into<BoxError>,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
Response::new(Body::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StreamBody<futures_util::stream::Empty<Result<Bytes, Error>>> {
|
||||
fn default() -> Self {
|
||||
Self::new(stream::empty())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> fmt::Debug for StreamBody<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("StreamBody").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> HttpBody for StreamBody<S>
|
||||
where
|
||||
S: TryStream,
|
||||
S::Ok: Into<Bytes>,
|
||||
S::Error: Into<BoxError>,
|
||||
{
|
||||
type Data = Bytes;
|
||||
type Error = Error;
|
||||
|
||||
fn poll_data(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||
let stream = self.project().stream.get_pin_mut();
|
||||
match ready!(stream.try_poll_next(cx)) {
|
||||
Some(Ok(chunk)) => Poll::Ready(Some(Ok(chunk.into()))),
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(Error::new(err)))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_trailers(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
|
||||
Poll::Ready(Ok(None))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_body_traits() {
|
||||
use futures_util::stream::Empty;
|
||||
|
||||
type EmptyStream = StreamBody<Empty<Result<Bytes, BoxError>>>;
|
||||
|
||||
crate::test_helpers::assert_send::<EmptyStream>();
|
||||
crate::test_helpers::assert_sync::<EmptyStream>();
|
||||
crate::test_helpers::assert_unpin::<EmptyStream>();
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use std::{convert::Infallible, fmt};
|
||||
|
||||
use crate::extract::Request;
|
||||
use crate::util::AxumMutex;
|
||||
use tower::Service;
|
||||
|
||||
use crate::{
|
||||
|
@ -9,7 +10,7 @@ use crate::{
|
|||
Router,
|
||||
};
|
||||
|
||||
pub(crate) struct BoxedIntoRoute<S, E>(Box<dyn ErasedIntoRoute<S, E>>);
|
||||
pub(crate) struct BoxedIntoRoute<S, E>(AxumMutex<Box<dyn ErasedIntoRoute<S, E>>>);
|
||||
|
||||
impl<S> BoxedIntoRoute<S, Infallible>
|
||||
where
|
||||
|
@ -20,10 +21,10 @@ where
|
|||
H: Handler<T, S>,
|
||||
T: 'static,
|
||||
{
|
||||
Self(Box::new(MakeErasedHandler {
|
||||
Self(AxumMutex::new(Box::new(MakeErasedHandler {
|
||||
handler,
|
||||
into_route: |handler, state| Route::new(Handler::with_state(handler, state)),
|
||||
}))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,20 +36,20 @@ impl<S, E> BoxedIntoRoute<S, E> {
|
|||
F: FnOnce(Route<E>) -> Route<E2> + Clone + Send + 'static,
|
||||
E2: 'static,
|
||||
{
|
||||
BoxedIntoRoute(Box::new(Map {
|
||||
inner: self.0,
|
||||
BoxedIntoRoute(AxumMutex::new(Box::new(Map {
|
||||
inner: self.0.into_inner().unwrap(),
|
||||
layer: Box::new(f),
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn into_route(self, state: S) -> Route<E> {
|
||||
self.0.into_route(state)
|
||||
self.0.into_inner().unwrap().into_route(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> Clone for BoxedIntoRoute<S, E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone_box())
|
||||
Self(AxumMutex::new(self.0.lock().unwrap().clone_box()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +64,7 @@ pub(crate) trait ErasedIntoRoute<S, E>: Send {
|
|||
|
||||
fn into_route(self: Box<Self>, state: S) -> Route<E>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn call_with_state(self: Box<Self>, request: Request, state: S) -> RouteFuture<E>;
|
||||
}
|
||||
|
||||
|
@ -118,7 +120,7 @@ where
|
|||
(self.into_route)(self.router, state)
|
||||
}
|
||||
|
||||
fn call_with_state(mut self: Box<Self>, request: Request, state: S) -> RouteFuture<Infallible> {
|
||||
fn call_with_state(self: Box<Self>, request: Request, state: S) -> RouteFuture<Infallible> {
|
||||
self.router.call_with_state(request, state)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ For a function to be used as a handler it must implement the [`Handler`] trait.
|
|||
axum provides blanket implementations for functions that:
|
||||
|
||||
- Are `async fn`s.
|
||||
- Take no more than 16 arguments that all implement [`FromRequest`].
|
||||
- Take no more than 16 arguments that all implement `Send`.
|
||||
- All except the last argument implement [`FromRequestParts`].
|
||||
- The last argument implements [`FromRequest`].
|
||||
- Returns something that implements [`IntoResponse`].
|
||||
- If a closure is used it must implement `Clone + Send` and be
|
||||
`'static`.
|
||||
|
|
|
@ -43,10 +43,10 @@ that can ultimately be converted to `Response`. This allows using `?` operator
|
|||
in handlers. See those examples:
|
||||
|
||||
* [`anyhow-error-response`][anyhow] for generic boxed errors
|
||||
* [`error-handling-and-dependency-injection`][ehdi] for application-specific detailed errors
|
||||
* [`error-handling`][error-handling] for application-specific detailed errors
|
||||
|
||||
[anyhow]: https://github.com/tokio-rs/axum/blob/main/examples/anyhow-error-response/src/main.rs
|
||||
[ehdi]: https://github.com/tokio-rs/axum/blob/main/examples/error-handling-and-dependency-injection/src/main.rs
|
||||
[error-handling]: https://github.com/tokio-rs/axum/blob/main/examples/error-handling/src/main.rs
|
||||
|
||||
This also applies to extractors. If an extractor doesn't match the request the
|
||||
request will be rejected and a response will be returned without calling your
|
||||
|
@ -92,7 +92,7 @@ let app = Router::new().route_service(
|
|||
async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
format!("Something went wrong: {err}"),
|
||||
)
|
||||
}
|
||||
# let _: Router = app;
|
||||
|
@ -133,7 +133,7 @@ async fn handle_timeout_error(err: BoxError) -> (StatusCode, String) {
|
|||
} else {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", err),
|
||||
format!("Unhandled internal error: {err}"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -174,7 +174,7 @@ async fn handle_timeout_error(
|
|||
) -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("`{} {}` failed with {}", method, uri, err),
|
||||
format!("`{method} {uri}` failed with {err}"),
|
||||
)
|
||||
}
|
||||
# let _: Router = app;
|
||||
|
|
|
@ -12,7 +12,6 @@ Types and traits for extracting data from requests.
|
|||
- [Defining custom extractors](#defining-custom-extractors)
|
||||
- [Accessing other extractors in `FromRequest` or `FromRequestParts` implementations](#accessing-other-extractors-in-fromrequest-or-fromrequestparts-implementations)
|
||||
- [Request body limits](#request-body-limits)
|
||||
- [Request body extractors](#request-body-extractors)
|
||||
- [Wrapping extractors](#wrapping-extractors)
|
||||
- [Logging rejections](#logging-rejections)
|
||||
|
||||
|
@ -20,8 +19,7 @@ Types and traits for extracting data from requests.
|
|||
|
||||
A handler function is an async function that takes any number of
|
||||
"extractors" as arguments. An extractor is a type that implements
|
||||
[`FromRequest`](crate::extract::FromRequest)
|
||||
or [`FromRequestParts`](crate::extract::FromRequestParts).
|
||||
[`FromRequest`] or [`FromRequestParts`].
|
||||
|
||||
For example, [`Json`] is an extractor that consumes the request body and
|
||||
deserializes it as JSON into some target type:
|
||||
|
@ -285,7 +283,7 @@ let app = Router::new().route("/users", post(create_user));
|
|||
# Customizing extractor responses
|
||||
|
||||
If an extractor fails it will return a response with the error and your
|
||||
handler will not be called. To customize the error response you have a two
|
||||
handler will not be called. To customize the error response you have two
|
||||
options:
|
||||
|
||||
1. Use `Result<T, T::Rejection>` as your extractor like shown in ["Optional
|
||||
|
@ -569,7 +567,7 @@ let app = Router::new().route(
|
|||
|
||||
# Accessing other extractors in `FromRequest` or `FromRequestParts` implementations
|
||||
|
||||
When defining custom extractors you often need to access another extractors
|
||||
When defining custom extractors you often need to access another extractor
|
||||
in your implementation.
|
||||
|
||||
```rust
|
||||
|
@ -707,9 +705,9 @@ async fn handler(
|
|||
# Logging rejections
|
||||
|
||||
All built-in extractors will log rejections for easier debugging. To see the
|
||||
logs, enable the `tracing` feature for axum and the `axum::rejection=trace`
|
||||
tracing target, for example with `RUST_LOG=info,axum::rejection=trace cargo
|
||||
run`.
|
||||
logs, enable the `tracing` feature for axum (enabled by default) and the
|
||||
`axum::rejection=trace` tracing target, for example with
|
||||
`RUST_LOG=info,axum::rejection=trace cargo run`.
|
||||
|
||||
[`body::Body`]: crate::body::Body
|
||||
[`Bytes`]: crate::body::Bytes
|
||||
|
|
|
@ -16,11 +16,9 @@ let handler = get(|| async {}).fallback(fallback);
|
|||
let app = Router::new().route("/", handler);
|
||||
|
||||
async fn fallback(method: Method, uri: Uri) -> (StatusCode, String) {
|
||||
(StatusCode::NOT_FOUND, format!("`{}` not allowed for {}", method, uri))
|
||||
(StatusCode::NOT_FOUND, format!("`{method}` not allowed for {uri}"))
|
||||
}
|
||||
# async {
|
||||
# hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
# };
|
||||
# let _: Router = app;
|
||||
```
|
||||
|
||||
## When used with `MethodRouter::merge`
|
||||
|
@ -44,10 +42,7 @@ let method_route = one.merge(two);
|
|||
|
||||
async fn fallback_one() -> impl IntoResponse { /* ... */ }
|
||||
async fn fallback_two() -> impl IntoResponse { /* ... */ }
|
||||
# let app = axum::Router::new().route("/", method_route);
|
||||
# async {
|
||||
# hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
# };
|
||||
# let app: axum::Router = axum::Router::new().route("/", method_route);
|
||||
```
|
||||
|
||||
## Setting the `Allow` header
|
||||
|
|
|
@ -19,7 +19,5 @@ let app = Router::new().route("/", merged);
|
|||
// Our app now accepts
|
||||
// - GET /
|
||||
// - POST /
|
||||
# async {
|
||||
# hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
# };
|
||||
# let _: Router = app;
|
||||
```
|
||||
|
|
|
@ -16,7 +16,7 @@ axum is unique in that it doesn't have its own bespoke middleware system and
|
|||
instead integrates with [`tower`]. This means the ecosystem of [`tower`] and
|
||||
[`tower-http`] middleware all work with axum.
|
||||
|
||||
While its not necessary to fully understand tower to write or use middleware
|
||||
While it's not necessary to fully understand tower to write or use middleware
|
||||
with axum, having at least a basic understanding of tower's concepts is
|
||||
recommended. See [tower's guides][tower-guides] for a general introduction.
|
||||
Reading the documentation for [`tower::ServiceBuilder`] is also recommended.
|
||||
|
@ -31,7 +31,7 @@ axum allows you to add middleware just about anywhere
|
|||
|
||||
## Applying multiple middleware
|
||||
|
||||
Its recommended to use [`tower::ServiceBuilder`] to apply multiple middleware at
|
||||
It's recommended to use [`tower::ServiceBuilder`] to apply multiple middleware at
|
||||
once, instead of calling `layer` (or `route_layer`) repeatedly:
|
||||
|
||||
```rust
|
||||
|
@ -64,14 +64,11 @@ Some commonly used middleware are:
|
|||
|
||||
- [`TraceLayer`](tower_http::trace) for high level tracing/logging.
|
||||
- [`CorsLayer`](tower_http::cors) for handling CORS.
|
||||
- [`CompressionLayer`](tower_http::compression) for automatic compression of
|
||||
responses.
|
||||
- [`CompressionLayer`](tower_http::compression) for automatic compression of responses.
|
||||
- [`RequestIdLayer`](tower_http::request_id) and
|
||||
[`PropagateRequestIdLayer`](tower_http::request_id) set and propagate request
|
||||
ids.
|
||||
- [`TimeoutLayer`](tower::timeout::TimeoutLayer) for timeouts. Note this
|
||||
requires using [`HandleErrorLayer`](crate::error_handling::HandleErrorLayer)
|
||||
to convert timeouts to responses.
|
||||
- [`TimeoutLayer`](tower_http::timeout::TimeoutLayer) for timeouts.
|
||||
|
||||
# Ordering
|
||||
|
||||
|
@ -131,9 +128,9 @@ That is:
|
|||
|
||||
It's a little more complicated in practice because any middleware is free to
|
||||
return early and not call the next layer, for example if a request cannot be
|
||||
authorized, but its a useful mental model to have.
|
||||
authorized, but it's a useful mental model to have.
|
||||
|
||||
As previously mentioned its recommended to add multiple middleware using
|
||||
As previously mentioned it's recommended to add multiple middleware using
|
||||
`tower::ServiceBuilder`, however this impacts ordering:
|
||||
|
||||
```rust
|
||||
|
@ -267,6 +264,21 @@ where
|
|||
}
|
||||
```
|
||||
|
||||
Note that your error type being defined as `S::Error` means that your middleware typically _returns no errors_. As a principle always try to return a response and try not to bail out with a custom error type. For example, if a 3rd party library you are using inside your new middleware returns its own specialized error type, try to convert it to some reasonable response and return `Ok` with that response.
|
||||
|
||||
If you choose to implement a custom error type such as `type Error = BoxError` (a boxed opaque error), or any other error type that is not `Infallible`, you must use a `HandleErrorLayer`, here is an example using a `ServiceBuilder`:
|
||||
|
||||
```ignore
|
||||
ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|_: BoxError| async {
|
||||
// because Axum uses infallible errors, you must handle your custom error type from your middleware here
|
||||
StatusCode::BAD_REQUEST
|
||||
}))
|
||||
.layer(
|
||||
// <your actual layer which DOES return an error>
|
||||
);
|
||||
```
|
||||
|
||||
## `tower::Service` and custom futures
|
||||
|
||||
If you're comfortable implementing your own futures (or want to learn it) and
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue