Merge branch 'main' into impl-status

This commit is contained in:
htrefil 2024-06-10 20:40:01 +02:00 committed by GitHub
commit daeb2c7f0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
237 changed files with 5598 additions and 2547 deletions

21
.github/DISCUSSION_TEMPLATE/q-a.yml vendored Normal file
View 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'
> ```

View file

@ -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

View file

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

View file

@ -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
View file

@ -0,0 +1,6 @@
[files]
extend-exclude = ["Cargo.toml"]
[default.extend-identifiers]
DefaultOnFailedUpdgrade = "DefaultOnFailedUpdgrade"
OnFailedUpdgrade = "OnFailedUpdgrade"

View file

@ -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.

View file

@ -2,14 +2,14 @@
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Core types and traits for axum"
edition = "2021"
rust-version = "1.56"
rust-version = "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"]

View file

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

View file

@ -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]

View file

@ -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()
}
}

View file

@ -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),

View file

@ -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> {

View file

@ -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 {

View file

@ -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);
}

View file

@ -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())
}
}

View file

@ -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))]

View file

@ -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);
}
}

View file

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

View file

@ -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()
}

View file

@ -44,7 +44,7 @@ use std::{convert::Infallible, fmt};
/// }
/// }
///
/// // Its also recommended to implement `IntoResponse` so `SetHeader` can be used on its own as
/// // It's also recommended to implement `IntoResponse` so `SetHeader` can be used on its own as
/// // the response
/// impl<'a> IntoResponse for SetHeader<'a> {
/// fn into_response(self) -> Response {
@ -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)
}
}

View file

@ -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;

View file

@ -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])

View file

@ -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

View file

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

View file

@ -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()
}
}

View file

@ -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},

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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);

View 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"
);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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`"

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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");
}
}

View file

@ -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()

View file

@ -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 {

View file

@ -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"],

View file

@ -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");
}

View file

@ -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()
}
}

View file

@ -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);
}
}

View file

@ -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");
}

View file

@ -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)

View file

@ -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"]

View file

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

View file

@ -1 +1 @@
nightly-2023-04-06
nightly-2024-03-13

View file

@ -1,21 +1,26 @@
use std::collections::HashSet;
use std::{collections::HashSet, fmt};
use crate::{
attr_parsing::{parse_assignment_attribute, second},
with_position::{Position, WithPosition},
};
use proc_macro2::{Span, TokenStream};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, Token, Type};
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, ReturnType, Token, Type};
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenStream {
let Attrs { state_ty } = attr;
let mut state_ty = state_ty.map(second);
let check_extractor_count = check_extractor_count(&item_fn);
let check_path_extractor = check_path_extractor(&item_fn);
let check_output_impls_into_response = check_output_impls_into_response(&item_fn);
let check_extractor_count = check_extractor_count(&item_fn, kind);
let check_path_extractor = check_path_extractor(&item_fn, kind);
let check_output_tuples = check_output_tuples(&item_fn);
let check_output_impls_into_response = if check_output_tuples.is_empty() {
check_output_impls_into_response(&item_fn)
} else {
check_output_tuples
};
// If the function is generic, we can't reliably check its inputs or whether the future it
// returns is `Send`. Skip those checks to avoid unhelpful additional compiler errors.
@ -32,8 +37,10 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
err = Some(
syn::Error::new(
Span::call_site(),
"can't infer state type, please add set it explicitly, as in \
`#[debug_handler(state = MyStateType)]`",
format!(
"can't infer state type, please add set it explicitly, as in \
`#[axum_macros::debug_{kind}(state = MyStateType)]`"
),
)
.into_compile_error(),
);
@ -43,16 +50,16 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
err.unwrap_or_else(|| {
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(()));
let check_future_send = check_future_send(&item_fn);
let check_future_send = check_future_send(&item_fn, kind);
if let Some(check_input_order) = check_input_order(&item_fn) {
if let Some(check_input_order) = check_input_order(&item_fn, kind) {
quote! {
#check_input_order
#check_future_send
}
} else {
let check_inputs_impls_from_request =
check_inputs_impls_from_request(&item_fn, state_ty);
check_inputs_impls_from_request(&item_fn, state_ty, kind);
quote! {
#check_inputs_impls_from_request
@ -63,17 +70,45 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
} else {
syn::Error::new_spanned(
&item_fn.sig.generics,
"`#[axum_macros::debug_handler]` doesn't support generic functions",
format!("`#[axum_macros::debug_{kind}]` doesn't support generic functions"),
)
.into_compile_error()
};
let middleware_takes_next_as_last_arg =
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
quote! {
#item_fn
#check_extractor_count
#check_path_extractor
#check_output_impls_into_response
#check_inputs_and_future_send
#middleware_takes_next_as_last_arg
}
}
#[derive(Clone, Copy)]
pub(crate) enum FunctionKind {
Handler,
Middleware,
}
impl fmt::Display for FunctionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FunctionKind::Handler => f.write_str("handler"),
FunctionKind::Middleware => f.write_str("middleware"),
}
}
}
impl FunctionKind {
fn name_uppercase_plural(&self) -> &'static str {
match self {
FunctionKind::Handler => "Handlers",
FunctionKind::Middleware => "Middleware",
}
}
}
@ -105,25 +140,36 @@ impl Parse for Attrs {
}
}
fn check_extractor_count(item_fn: &ItemFn) -> Option<TokenStream> {
fn check_extractor_count(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
let max_extractors = 16;
if item_fn.sig.inputs.len() <= max_extractors {
let inputs = item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind))
.count();
if inputs <= max_extractors {
None
} else {
let error_message = format!(
"Handlers cannot take more than {max_extractors} arguments. \
"{} cannot take more than {max_extractors} arguments. \
Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors",
kind.name_uppercase_plural(),
);
let error = syn::Error::new_spanned(&item_fn.sig.inputs, error_message).to_compile_error();
Some(error)
}
}
fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
fn extractor_idents(
item_fn: &ItemFn,
kind: FunctionKind,
) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
item_fn
.sig
.inputs
.iter()
.filter(move |arg| skip_next_arg(arg, kind))
.enumerate()
.filter_map(|(idx, fn_arg)| match fn_arg {
FnArg::Receiver(_) => None,
@ -141,8 +187,8 @@ fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnAr
})
}
fn check_path_extractor(item_fn: &ItemFn) -> TokenStream {
let path_extractors = extractor_idents(item_fn)
fn check_path_extractor(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
let path_extractors = extractor_idents(item_fn, kind)
.filter(|(_, _, ident)| *ident == "Path")
.collect::<Vec<_>>();
@ -174,121 +220,294 @@ fn is_self_pat_type(typed: &syn::PatType) -> bool {
ident == "self"
}
fn check_inputs_impls_from_request(item_fn: &ItemFn, state_ty: Type) -> TokenStream {
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");
}

View file

@ -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()?;

View file

@ -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)

View file

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

View file

@ -1,6 +1,6 @@
use axum_macros::debug_handler;
#[debug_handler]
async fn handler(foo: bool) {}
async fn handler(_foo: bool) {}
fn main() {}

View file

@ -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`

View file

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

View file

@ -0,0 +1,28 @@
error[E0277]: the trait bound `NonCloneType: Clone` is not satisfied
--> tests/debug_handler/fail/extension_not_clone.rs:7:38
|
7 | async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonCloneType`, which is required by `Extension<NonCloneType>: FromRequest<(), _>`
|
= help: the following other types implement trait `FromRequest<S, M>`:
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;
|

View file

@ -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() {}

View file

@ -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) {}
| ^^^

View file

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

View file

@ -1,20 +1,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`

View file

@ -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;
}

View file

@ -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
|

View file

@ -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() {}

View file

@ -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 | | ) {
| |_^

View file

@ -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(){}

View file

@ -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

View file

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

View file

@ -0,0 +1,21 @@
error[E0277]: the trait bound `NotIntoResponse: IntoResponse` is not satisfied
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
6 | async fn handler() -> (NotIntoResponse) {
| ^^^^^^^^^^^^^^^^^ the trait `IntoResponse` is not implemented for `NotIntoResponse`
|
= help: the following other types implement trait `IntoResponse`:
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`

View file

@ -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() {}

View file

@ -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,
| |______________^

View 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() {}

View file

@ -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`

View file

@ -5,14 +5,14 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
| ^^^^ the trait `IntoResponse` is not implemented for `bool`
|
= help: the following other types implement trait `IntoResponse`:
&'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

View file

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

View file

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

View file

@ -0,0 +1,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() {}

View file

@ -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 {
| ^^^^^^^^^^

View file

@ -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() {}

View file

@ -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)

View 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() {}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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]]

View file

@ -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

View file

@ -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);

View file

@ -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
View file

@ -0,0 +1,3 @@
disallowed-types = [
{ path = "std::sync::Mutex", reason = "Use our internal AxumMutex instead" },
]

View file

@ -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)
}

View file

@ -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>();
}

View file

@ -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)
}
}

View file

@ -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`.

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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;
```

View file

@ -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