Merge branch 'main' into main

This commit is contained in:
Jonas Platte 2024-09-20 23:19:20 +02:00 committed by GitHub
commit 361632f9ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
192 changed files with 3543 additions and 1285 deletions

View file

@ -2,7 +2,7 @@ name: CI
env:
CARGO_TERM_COLOR: always
MSRV: '1.66'
MSRV: '1.70'
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 }}
@ -139,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
@ -155,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
@ -189,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
@ -204,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
@ -224,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

@ -26,8 +26,9 @@ If your project isn't listed here and you would like it to be, please feel free
- [axum-guard-logic](https://github.com/sjud/axum_guard_logic): Use AND/OR logic to extract types and check their values against `Service` inputs.
- [axum-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.
- [tower-sessions](https://docs.rs/tower-sessions): Cookie-based sessions.
- [axum-login](https://docs.rs/axum-login): Session-based user authentication for axum.
- [axum-csrf-sync-pattern](https://crates.io/crates/axum-csrf-sync-pattern): A middleware implementing CSRF STP for AJAX backends and API endpoints.
- [axum-otel-metrics](https://github.com/ttys3/axum-otel-metrics/): A axum OpenTelemetry Metrics middleware with prometheus exporter supported.
@ -45,6 +46,10 @@ If your project isn't listed here and you would like it to be, please feel free
- [socketioxide](https://github.com/totodore/socketioxide): An easy to use socket.io server implementation working as a `tower` layer/service.
- [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.
- [spring-rs](https://github.com/spring-rs/spring-rs): spring-rs is a microservice framework written in rust inspired by java's spring-boot, based on axum
- [zino](https://github.com/zino-rs/zino): Zino is a next-generation framework for composable applications which provides full integrations with axum.
## Project showcase
@ -58,6 +63,8 @@ If your project isn't listed here and you would like it to be, please feel free
- [realworld-axum-sqlx](https://github.com/launchbadge/realworld-axum-sqlx): A Rust implementation of the [Realworld] demo app spec using Axum and [SQLx].
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
- [axum-postgres-template](https://github.com/koskeller/axum-postgres-template): Production-ready Axum + PostgreSQL application template
- [RUSTfulapi](https://github.com/robatipoor/rustfulapi): Reusable template for building REST Web Services in Rust. Uses Axum HTTP web framework and SeaORM.
- [Jotsy](https://github.com/ohsayan/jotsy): Self-hosted notes app powered by Skytable, Axum and Tokio
- [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!
@ -82,6 +89,7 @@ If your project isn't listed here and you would like it to be, please feel free
- [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.
- [Mini RPS](https://github.com/marcodpt/minirps): Mini reverse proxy server, HTTPS, CORS, static file hosting and template engine (minijinja).
[Realworld]: https://github.com/gothinkster/realworld
[SQLx]: https://github.com/launchbadge/sqlx

View file

@ -5,9 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
# 0.4.4
- None.
- **added:** Derive `Clone` and `Copy` for `AppendHeaders` ([#2776])
- **added:** `must_use` attribute on `AppendHeaders` ([#2846])
- **added:** `must_use` attribute on `ErrorResponse` ([#2846])
- **added:** `must_use` attribute on `IntoResponse::into_response` ([#2846])
- **added:** `must_use` attribute on `IntoResponseParts` trait methods ([#2846])
- **added:** Implement `Copy` for `DefaultBodyLimit` ([#2875])
- **added**: `DefaultBodyLimit::max` and `DefaultBodyLimit::disable` are now
allowed in const context ([#2875])
[#2776]: https://github.com/tokio-rs/axum/pull/2776
[#2846]: https://github.com/tokio-rs/axum/pull/2846
[#2875]: https://github.com/tokio-rs/axum/pull/2875
# 0.4.3 (13. January, 2024)
- **added:** Implement `IntoResponseParts` for `()` ([#2471])
[#2471]: https://github.com/tokio-rs/axum/pull/2471
# 0.4.2 (29. December, 2023)

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.4.2" # remember to also bump the version that axum and axum-extra depend on
version = "0.4.4" # remember to also bump the version that axum and axum-extra depend on
[features]
tracing = ["dep:tracing"]
@ -18,31 +18,29 @@ tracing = ["dep:tracing"]
__private_docs = ["dep:tower-http"]
[dependencies]
bytes = "1.0"
bytes = "1.2"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
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.5.0", optional = true, features = ["limit"] }
tower-http = { version = "0.6.0", optional = true, features = ["limit"] }
tracing = { version = "0.1.37", default-features = false, optional = true }
[build-dependencies]
rustversion = "1.0.9"
[dev-dependencies]
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 = "1.0.0"
tokio = { version = "1.25.0", features = ["macros"] }
tower-http = { version = "0.5.0", features = ["limit"] }
tower-http = { version = "0.6.0", features = ["limit"] }
[package.metadata.cargo-public-api-crates]
allowed = [

View file

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

View file

@ -28,6 +28,7 @@ mod tests {
}
// some extractor that requires the state, such as `SignedCookieJar`
#[allow(dead_code)]
pub(crate) struct RequiresState(pub(crate) String);
impl<S> FromRequestParts<S> for RequiresState

View file

@ -72,7 +72,7 @@ use tower_layer::Layer;
/// [`RequestBodyLimit`]: tower_http::limit::RequestBodyLimit
/// [`RequestExt::with_limited_body`]: crate::RequestExt::with_limited_body
/// [`RequestExt::into_limited_body`]: crate::RequestExt::into_limited_body
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct DefaultBodyLimit {
kind: DefaultBodyLimitKind,
@ -116,7 +116,7 @@ impl DefaultBodyLimit {
/// [`Bytes`]: bytes::Bytes
/// [`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 {
pub const fn disable() -> Self {
Self {
kind: DefaultBodyLimitKind::Disable,
}
@ -149,7 +149,7 @@ impl DefaultBodyLimit {
/// [`Bytes::from_request`]: bytes::Bytes
/// [`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 {
pub const fn max(limit: usize) -> Self {
Self {
kind: DefaultBodyLimitKind::Limit(limit),
}

View file

@ -42,8 +42,8 @@ mod private {
/// See [`axum::extract`] for more general docs about extractors.
///
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
#[cfg_attr(
nightly_error_messages,
#[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",
)
@ -71,8 +71,8 @@ pub trait FromRequestParts<S>: Sized {
/// See [`axum::extract`] for more general docs about extractors.
///
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
#[cfg_attr(
nightly_error_messages,
#[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",
)

View file

@ -1,6 +1,6 @@
use super::{rejection::*, FromRequest, FromRequestParts, Request};
use crate::{body::Body, RequestExt};
use bytes::Bytes;
use bytes::{BufMut, Bytes, BytesMut};
use http::{request::Parts, Extensions, HeaderMap, Method, Uri, Version};
use http_body_util::BodyExt;
use std::convert::Infallible;
@ -65,6 +65,36 @@ where
}
}
impl<S> FromRequest<S> for BytesMut
where
S: Send + Sync,
{
type Rejection = BytesRejection;
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
let mut body = req.into_limited_body();
let mut bytes = BytesMut::new();
body_to_bytes_mut(&mut body, &mut bytes).await?;
Ok(bytes)
}
}
async fn body_to_bytes_mut(body: &mut Body, bytes: &mut BytesMut) -> Result<(), BytesRejection> {
while let Some(frame) = body
.frame()
.await
.transpose()
.map_err(FailedToBufferBody::from_err)?
{
let Ok(data) = frame.into_data() else {
return Ok(());
};
bytes.put(data);
}
Ok(())
}
impl<S> FromRequest<S> for Bytes
where
S: Send + Sync,
@ -98,9 +128,7 @@ where
}
})?;
let string = std::str::from_utf8(&bytes)
.map_err(InvalidUtf8::from_err)?
.to_owned();
let string = String::from_utf8(bytes.into()).map_err(InvalidUtf8::from_err)?;
Ok(string)
}

View file

@ -1,4 +1,3 @@
#![cfg_attr(nightly_error_messages, feature(diagnostic_namespace))]
//! Core types and traits for [`axum`].
//!
//! Libraries authors that want to provide [`FromRequest`] or [`IntoResponse`] implementations
@ -22,7 +21,6 @@
clippy::needless_borrow,
clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock,
clippy::match_on_vec_items,
clippy::imprecise_flops,

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]
@ -303,8 +314,6 @@ mod composite_rejection_tests {
#[allow(dead_code, unreachable_pub)]
mod defs {
use crate::{__composite_rejection, __define_rejection};
__define_rejection! {
#[status = BAD_REQUEST]
#[body = "error message 1"]

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

@ -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()
/// }
/// }
@ -111,6 +111,7 @@ use std::{
/// ```
pub trait IntoResponse {
/// Create a response.
#[must_use]
fn into_response(self) -> Response;
}

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 {
@ -105,21 +105,25 @@ pub struct ResponseParts {
impl ResponseParts {
/// Gets a reference to the response headers.
#[must_use]
pub fn headers(&self) -> &HeaderMap {
self.res.headers()
}
/// Gets a mutable reference to the response headers.
#[must_use]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
self.res.headers_mut()
}
/// Gets a reference to the response extensions.
#[must_use]
pub fn extensions(&self) -> &Extensions {
self.res.extensions()
}
/// Gets a mutable reference to the response extensions.
#[must_use]
pub fn extensions_mut(&mut self) -> &mut Extensions {
self.res.extensions_mut()
}
@ -258,3 +262,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

@ -117,6 +117,7 @@ where
///
/// See [`Result`] for more details.
#[derive(Debug)]
#[must_use]
pub struct ErrorResponse(Response);
impl<T> From<T> for ErrorResponse

View file

@ -7,7 +7,32 @@ and this project adheres to [Semantic Versioning].
# Unreleased
- None.
- **breaking:** Update to prost 0.13. Used for the `Protobuf` extractor ([#2829])
- **change:** Update minimum rust version to 1.70 ([#2829])
[#2829]: https://github.com/tokio-rs/axum/pull/2829
# 0.9.4
- **added:** The `response::Attachment` type ([#2789])
[#2789]: https://github.com/tokio-rs/axum/pull/2789
# 0.9.3 (24. March, 2024)
- **added:** New `tracing` feature which enables logging rejections from
built-in extractor with the `axum::rejection=trace` target ([#2584])
[#2584]: https://github.com/tokio-rs/axum/pull/2584
# 0.9.2 (13. January, 2024)
- **added:** Implement `TypedPath` for `WithRejection<TypedPath, _>`
- **fixed:** Documentation link to `serde::Deserialize` in `JsonDeserializer` extractor ([#2498])
- **added:** Add `is_missing` function for `TypedHeaderRejection` and `TypedHeaderRejectionReason` ([#2503])
[#2498]: https://github.com/tokio-rs/axum/pull/2498
[#2503]: https://github.com/tokio-rs/axum/pull/2503
# 0.9.1 (29. December, 2023)

View file

@ -2,19 +2,20 @@
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Extra utilities for axum"
edition = "2021"
rust-version = "1.66"
rust-version = "1.70"
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.9.1"
version = "0.9.4"
[features]
default = []
default = ["tracing"]
async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"]
attachment = ["dep:tracing"]
cookie = ["dep:cookie"]
cookie-private = ["cookie", "cookie?/private"]
cookie-signed = ["cookie", "cookie?/signed"]
@ -33,12 +34,13 @@ json-lines = [
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.7.2", default-features = false }
axum-core = { path = "../axum-core", version = "0.4.2" }
axum = { path = "../axum", version = "0.7.6", default-features = false }
axum-core = { path = "../axum-core", version = "0.4.4" }
bytes = "1.1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0"
@ -47,34 +49,35 @@ http-body-util = "0.1.0"
mime = "0.3"
pin-project-lite = "0.2"
serde = "1.0"
tower = { version = "0.4", default_features = false, features = ["util"] }
tower = { version = "0.5.1", default-features = false, features = ["util"] }
tower-layer = "0.3"
tower-service = "0.3"
# optional dependencies
axum-macros = { path = "../axum-macros", version = "0.4.0", optional = true }
axum-macros = { path = "../axum-macros", version = "0.4.2", 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.4.0", optional = true }
multer = { version = "3.0.0", optional = true }
percent-encoding = { version = "2.1", optional = true }
prost = { version = "0.12", optional = true }
prost = { version = "0.13", optional = true }
serde_html_form = { version = "0.2.0", optional = true }
serde_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.7.2" }
hyper = "1.0.0"
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "multipart"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.71"
tokio = { version = "1.14", features = ["full"] }
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.5.0", features = ["map-response-body", "timeout"] }
tower = { version = "0.5.1", features = ["util"] }
tower-http = { version = "0.6.0", features = ["map-response-body", "timeout"] }
[package.metadata.docs.rs]
all-features = true

View file

@ -104,8 +104,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

@ -79,11 +79,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()
}
}
}
}
@ -111,7 +116,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]
@ -133,7 +138,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

@ -9,7 +9,7 @@ use std::marker::PhantomData;
/// JSON Extractor for zero-copy deserialization.
///
/// Deserialize request bodies into some type that implements [`serde::Deserialize<'de>`].
/// Deserialize request bodies into some type that implements [`serde::Deserialize<'de>`][serde::Deserialize].
/// Parsing JSON is delayed until [`deserialize`](JsonDeserializer::deserialize) is called.
/// If the type implements [`serde::de::DeserializeOwned`], the [`Json`](axum::Json) extractor should
/// be preferred.
@ -22,8 +22,7 @@ use std::marker::PhantomData;
/// 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.
/// - 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
@ -243,7 +242,7 @@ mod tests {
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client.post("/").json(&json!({ "foo": "bar" })).send().await;
let res = client.post("/").json(&json!({ "foo": "bar" })).await;
let body = res.text().await;
assert_eq!(body, "bar");
@ -275,11 +274,7 @@ mod tests {
let client = TestClient::new(app);
// The escaped characters prevent serde_json from borrowing.
let res = client
.post("/")
.json(&json!({ "foo": "\"bar\"" }))
.send()
.await;
let res = client.post("/").json(&json!({ "foo": "\"bar\"" })).await;
let body = res.text().await;
@ -306,19 +301,11 @@ mod tests {
let client = TestClient::new(app);
let res = client
.post("/")
.json(&json!({ "foo": "good" }))
.send()
.await;
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\"" }))
.send()
.await;
let res = client.post("/").json(&json!({ "foo": "\"bad\"" })).await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let body_text = res.text().await;
assert_eq!(
@ -342,7 +329,7 @@ mod tests {
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client.post("/").body(r#"{ "foo": "bar" }"#).send().await;
let res = client.post("/").body(r#"{ "foo": "bar" }"#).await;
let status = res.status();
@ -364,7 +351,6 @@ mod tests {
.post("/")
.header("content-type", content_type)
.body("{}")
.send()
.await;
res.status() == StatusCode::OK
@ -393,7 +379,6 @@ mod tests {
.post("/")
.body("{")
.header("content-type", "application/json")
.send()
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@ -431,7 +416,6 @@ mod tests {
.post("/")
.body("{\"a\": 1, \"b\": [{\"x\": 2}]}")
.header("content-type", "application/json")
.send()
.await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);

View file

@ -377,7 +377,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()
}
}
@ -405,7 +411,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() {
@ -435,7 +441,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
@ -464,7 +470,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

@ -79,19 +79,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

@ -50,6 +50,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);
@ -85,11 +112,16 @@ pub enum QueryRejection {
impl IntoResponse for QueryRejection {
fn into_response(self) -> Response {
match self {
Self::FailedToDeserializeQueryString(inner) => (
StatusCode::BAD_REQUEST,
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()
}
}
}
}
@ -232,7 +264,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]
@ -254,7 +286,6 @@ mod tests {
.post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
@ -283,7 +314,6 @@ mod tests {
.post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
@ -309,7 +339,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.post("/").body("").send().await;
let res = client.post("/").body("").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "None");
@ -338,7 +368,6 @@ mod tests {
.post("/?other=something")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.send()
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);

View file

@ -1,10 +1,13 @@
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
@ -134,15 +137,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

@ -54,7 +54,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> {
@ -182,7 +182,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 {
@ -222,7 +222,6 @@ mod tests {
]
.join("\n"),
)
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -243,7 +242,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

@ -21,6 +21,7 @@
//! `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
//!
@ -39,7 +40,6 @@
clippy::needless_borrow,
clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock,
clippy::match_on_vec_items,
clippy::imprecise_flops,
@ -96,11 +96,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;
@ -115,9 +114,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

@ -199,7 +199,6 @@ mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::post, Router};
use http::StatusCode;
#[tokio::test]
async fn decode_body() {
@ -219,7 +218,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;
@ -247,7 +246,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);
}
@ -282,7 +281,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

@ -0,0 +1,103 @@
use axum::response::IntoResponse;
use http::{header, HeaderMap, HeaderValue};
use tracing::error;
/// A file attachment response.
///
/// This type will set the `Content-Disposition` header to `attachment`. In response a webbrowser
/// will offer to download the file instead of displaying it directly.
///
/// Use the `filename` and `content_type` methods to set the filename or content-type of the
/// attachment. If these values are not set they will not be sent.
///
///
/// # Example
///
/// ```rust
/// use axum::{http::StatusCode, routing::get, Router};
/// use axum_extra::response::Attachment;
///
/// async fn cargo_toml() -> Result<Attachment<String>, (StatusCode, String)> {
/// let file_contents = tokio::fs::read_to_string("Cargo.toml")
/// .await
/// .map_err(|err| (StatusCode::NOT_FOUND, format!("File not found: {err}")))?;
/// Ok(Attachment::new(file_contents)
/// .filename("Cargo.toml")
/// .content_type("text/x-toml"))
/// }
///
/// let app = Router::new().route("/Cargo.toml", get(cargo_toml));
/// let _: Router = app;
/// ```
///
/// # Note
///
/// If you use axum with hyper, hyper will set the `Content-Length` if it is known.
#[derive(Debug)]
#[must_use]
pub struct Attachment<T> {
inner: T,
filename: Option<HeaderValue>,
content_type: Option<HeaderValue>,
}
impl<T: IntoResponse> Attachment<T> {
/// Creates a new [`Attachment`].
pub fn new(inner: T) -> Self {
Self {
inner,
filename: None,
content_type: None,
}
}
/// Sets the filename of the [`Attachment`].
///
/// This updates the `Content-Disposition` header to add a filename.
pub fn filename<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
self.filename = if let Ok(filename) = value.try_into() {
Some(filename)
} else {
error!("Attachment filename contains invalid characters");
None
};
self
}
/// Sets the content-type of the [`Attachment`]
pub fn content_type<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
if let Ok(content_type) = value.try_into() {
self.content_type = Some(content_type);
} else {
error!("Attachment content-type contains invalid characters");
}
self
}
}
impl<T> IntoResponse for Attachment<T>
where
T: IntoResponse,
{
fn into_response(self) -> axum::response::Response {
let mut headers = HeaderMap::new();
if let Some(content_type) = self.content_type {
headers.append(header::CONTENT_TYPE, content_type);
}
let content_disposition = if let Some(filename) = self.filename {
let mut bytes = b"attachment; filename=\"".to_vec();
bytes.extend_from_slice(filename.as_bytes());
bytes.push(b'\"');
HeaderValue::from_bytes(&bytes).expect("This was a HeaderValue so this can not fail")
} else {
HeaderValue::from_static("attachment")
};
headers.append(header::CONTENT_DISPOSITION, content_disposition);
(headers, self.inner).into_response()
}
}

View file

@ -3,6 +3,9 @@
#[cfg(feature = "erased-json")]
mod erased_json;
#[cfg(feature = "attachment")]
mod attachment;
#[cfg(feature = "erased-json")]
pub use erased_json::ErasedJson;
@ -10,6 +13,9 @@ pub use erased_json::ErasedJson;
#[doc(no_inline)]
pub use crate::json_lines::JsonLines;
#[cfg(feature = "attachment")]
pub use attachment::Attachment;
macro_rules! mime_response {
(
$(#[$m:meta])*

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,7 +149,7 @@ 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;

View file

@ -85,12 +85,12 @@ use serde::Serialize;
///
/// - A `TypedPath` implementation.
/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`],
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
/// also implement [`serde::Deserialize`], unless it's a unit struct.
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
/// also implement [`serde::Deserialize`], unless it's a unit struct.
/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other
/// things, create links to known paths and have them verified statically. Note that the
/// [`Display`] implementation for each field must return something that's compatible with its
/// [`Deserialize`] implementation.
/// things, create links to known paths and have them verified statically. Note that the
/// [`Display`] implementation for each field must return something that's compatible with its
/// [`Deserialize`] implementation.
///
/// Additionally the macro will verify the captures in the path matches the fields of the struct.
/// For example this fails to compile since the struct doesn't have a `team_id` field:
@ -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

@ -5,7 +5,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`].
@ -121,6 +121,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`]
@ -134,9 +142,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()
}
}
@ -166,7 +187,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() {
@ -188,7 +209,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!(
@ -196,11 +216,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

@ -5,9 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
# 0.4.2
- None.
- **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)

View file

@ -9,7 +9,7 @@ license = "MIT"
name = "axum-macros"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.4.0" # remember to also bump the version that axum and axum-extra depends on
version = "0.4.2" # remember to also bump the version that axum and axum-extra depends on
[features]
default = []
@ -19,7 +19,6 @@ __private = ["syn/visit-mut"]
proc-macro = true
[dependencies]
heck = "0.4"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = [

View file

@ -1 +1 @@
nightly-2023-11-23
nightly-2024-06-22

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

@ -54,7 +54,7 @@ fn expand_field(state: &Ident, idx: usize, field: &Field) -> TokenStream {
};
quote_spanned! {span=>
#[allow(clippy::clone_on_copy)]
#[allow(clippy::clone_on_copy, clippy::clone_on_ref_ptr)]
impl ::axum::extract::FromRef<#state> for #field_ty {
fn from_ref(state: &#state) -> Self {
#body

View file

@ -181,7 +181,7 @@ pub(crate) fn expand(item: syn::Item, tr: Trait) -> syn::Result<TokenStream> {
variants,
} = item;
let generics_error = format!("`#[derive({tr})] on enums don't support generics");
let generics_error = format!("`#[derive({tr})]` on enums don't support generics");
if !generics.params.is_empty() {
return Err(syn::Error::new_spanned(generics, generics_error));
@ -1022,7 +1022,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()?;
@ -1032,7 +1032,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

@ -15,7 +15,6 @@
clippy::needless_borrow,
clippy::match_wildcard_for_single_variants,
clippy::if_let_mutex,
clippy::mismatched_target_os,
clippy::await_holding_lock,
clippy::match_on_vec_items,
clippy::imprecise_flops,
@ -44,6 +43,7 @@
#![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 +233,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,
@ -349,7 +398,7 @@ use from_request::Trait::{FromRequest, FromRequestParts};
///
/// # Known limitations
///
/// Generics are only supported on tuple structs with exactly on field. Thus this doesn't work
/// Generics are only supported on tuple structs with exactly one field. Thus this doesn't work
///
/// ```compile_fail
/// #[derive(axum_macros::FromRequest)]
@ -415,7 +464,7 @@ 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 +515,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]
@ -569,7 +616,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!

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,4 +1,3 @@
#![feature(diagnostic_namespace)]
use axum_macros::debug_handler;
#[debug_handler]

View file

@ -1,24 +1,24 @@
error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied
--> tests/debug_handler/fail/argument_not_extractor.rs:5:24
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
|
5 | 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/0.7/axum/extract/index.html` for details
= help: the following other types implement trait `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>>
<ConnectInfo<T> as FromRequestParts<S>>
`()` implements `FromRequestParts<S>`
`(T1, T2)` implements `FromRequestParts<S>`
`(T1, T2, T3)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
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:5:24
--> tests/debug_handler/fail/argument_not_extractor.rs:4:24
|
5 | async fn handler(_foo: bool) {}
4 | async fn handler(_foo: bool) {}
| ^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`

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>`:
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
(T1, T2, T3, T4, T5, T6, T7, T8, T9)
and $N others
= note: required for `Extension<NonCloneType>` to implement `FromRequestParts<()>`
= note: required for `Extension<NonCloneType>` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_test_extension_non_clone_0_from_request_check`
--> tests/debug_handler/fail/extension_not_clone.rs:7:38
|
7 | async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
| ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `__axum_macros_check_test_extension_non_clone_0_from_request_check`
help: consider annotating `NonCloneType` with `#[derive(Clone)]`
|
4 + #[derive(Clone)]
5 | struct NonCloneType;
|

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,51 @@
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<()>`
|
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `Struct` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `serde::de::Deserialize<'de>`:
bool
char
isize
i8
i16
i32
i64
i128
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `Struct` to implement `serde::de::DeserializeOwned`
= note: required for `Json<Struct>` to implement `FromRequest<()>`
= help: see issue #48214
= help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
1 + #![feature(trivial_bounds)]
|
error[E0277]: the trait bound `for<'de> Struct: serde::de::Deserialize<'de>` is not satisfied
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
|
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `Struct` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `serde::de::Deserialize<'de>`:
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `Struct` to implement `serde::de::DeserializeOwned`
= note: required for `Json<Struct>` to implement `FromRequest<()>`
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`

View file

@ -4,7 +4,7 @@ error: future cannot be sent between threads safely
3 | #[debug_handler]
| ^^^^^^^^^^^^^^^^ future returned by `handler` is not `Send`
|
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`
= 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:14
|
@ -12,8 +12,6 @@ note: future is not `Send` as this value is used across an await
| --- has type `Rc<()>` which is not `Send`
6 | async {}.await;
| ^^^^^ await occurs here, with `_rc` maybe used later
7 | }
| - `_rc` is later dropped here
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`:
&'static [u8; N]
&'static [u8]
&'static str
()
(R,)
(Response<()>, R)
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
and $N others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
6 | async fn handler() -> (NotIntoResponse) {
| ^^^^^^^^^^^^^^^^^ required by this bound in `check`

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,49 @@
error: `Json<_>` must be the last element in a response tuple
--> tests/debug_handler/fail/wrong_return_tuple.rs:6:5
|
6 | axum::Json<&'static str>,
| ^^^^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not satisfied
--> tests/debug_handler/fail/wrong_return_tuple.rs:24:5
|
24 | CustomIntoResponse,
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
= help: the following other types implement trait `IntoResponseParts`:
()
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
and $N others
= help: see issue #48214
help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
3 + #![feature(trivial_bounds)]
|
error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not satisfied
--> tests/debug_handler/fail/wrong_return_tuple.rs:24:5
|
24 | CustomIntoResponse,
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
|
= help: the following other types implement trait `IntoResponseParts`:
()
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
and $N others
note: required by a bound in `__axum_macros_check_custom_type_into_response_parts_1_check`
--> tests/debug_handler/fail/wrong_return_tuple.rs: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`:
Box<str>
Box<[u8]>
axum::body::Bytes
Body
axum::extract::rejection::FailedToBufferBody
axum::extract::rejection::LengthLimitError
axum::extract::rejection::UnknownBodyError
axum::extract::rejection::InvalidUtf8
&'static [u8; N]
&'static [u8]
&'static str
()
(R,)
(Response<()>, R)
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
and $N others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/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

@ -1,4 +1,3 @@
#![feature(diagnostic_namespace)]
use axum::{routing::get, Router};
use axum_macros::FromRequest;

View file

@ -1,21 +1,21 @@
error: #[derive(FromRequest)] only supports generics when used with #[from_request(via)]
--> tests/from_request/fail/generic_without_via.rs:6:18
--> tests/from_request/fail/generic_without_via.rs:5:18
|
6 | struct Extractor<T>(T);
5 | struct Extractor<T>(T);
| ^
error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future<Output = ()> {foo}: Handler<_, _>` is not satisfied
--> tests/from_request/fail/generic_without_via.rs:11:44
--> tests/from_request/fail/generic_without_via.rs:10:44
|
11 | _ = Router::<()>::new().route("/", get(foo));
10 | _ = Router::<()>::new().route("/", get(foo));
| --- ^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Extractor<()>) -> impl Future<Output = ()> {foo}`
| |
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
<Layered<L, H, T, S> as Handler<T, S>>
<MethodRouter<S> as Handler<(), S>>
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `axum::routing::get`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|

View file

@ -1,4 +1,3 @@
#![feature(diagnostic_namespace)]
use axum::{routing::get, Router};
use axum_macros::FromRequest;

View file

@ -1,21 +1,21 @@
error: #[derive(FromRequest)] only supports generics when used with #[from_request(via)]
--> tests/from_request/fail/generic_without_via_rejection.rs:7:18
--> tests/from_request/fail/generic_without_via_rejection.rs:6:18
|
7 | struct Extractor<T>(T);
6 | struct Extractor<T>(T);
| ^
error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future<Output = ()> {foo}: Handler<_, _>` is not satisfied
--> tests/from_request/fail/generic_without_via_rejection.rs:12:44
--> tests/from_request/fail/generic_without_via_rejection.rs:11:44
|
12 | _ = Router::<()>::new().route("/", get(foo));
11 | _ = Router::<()>::new().route("/", get(foo));
| --- ^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Extractor<()>) -> impl Future<Output = ()> {foo}`
| |
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
<Layered<L, H, T, S> as Handler<T, S>>
<MethodRouter<S> as Handler<(), S>>
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `axum::routing::get`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|

View file

@ -1,4 +1,3 @@
#![feature(diagnostic_namespace)]
use axum::{
extract::rejection::ExtensionRejection,
response::{IntoResponse, Response},

View file

@ -1,21 +1,21 @@
error: cannot use `rejection` without `via`
--> tests/from_request/fail/override_rejection_on_enum_without_via.rs:19:16
--> tests/from_request/fail/override_rejection_on_enum_without_via.rs:18:16
|
19 | #[from_request(rejection(MyRejection))]
18 | #[from_request(rejection(MyRejection))]
| ^^^^^^^^^
error[E0277]: the trait bound `fn(MyExtractor) -> impl Future<Output = ()> {handler}: Handler<_, _>` is not satisfied
--> tests/from_request/fail/override_rejection_on_enum_without_via.rs:11:50
--> tests/from_request/fail/override_rejection_on_enum_without_via.rs:10:50
|
11 | let _: Router = Router::new().route("/", get(handler).post(handler_result));
10 | let _: Router = Router::new().route("/", get(handler).post(handler_result));
| --- ^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(MyExtractor) -> impl Future<Output = ()> {handler}`
| |
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
<Layered<L, H, T, S> as Handler<T, S>>
<MethodRouter<S> as Handler<(), S>>
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `axum::routing::get`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
@ -27,17 +27,17 @@ note: required by a bound in `axum::routing::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
--> tests/from_request/fail/override_rejection_on_enum_without_via.rs:11:64
--> tests/from_request/fail/override_rejection_on_enum_without_via.rs:10:64
|
11 | let _: Router = Router::new().route("/", get(handler).post(handler_result));
10 | let _: Router = Router::new().route("/", get(handler).post(handler_result));
| ---- ^^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Result<MyExtractor, MyRejection>) -> impl Future<Output = ()> {handler_result}`
| |
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
<Layered<L, H, T, S> as Handler<T, S>>
<MethodRouter<S> as Handler<(), S>>
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `MethodRouter::<S>::post`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|

View file

@ -1,4 +1,3 @@
#![feature(diagnostic_namespace)]
use axum::{extract::FromRequestParts, response::Response};
#[derive(FromRequestParts)]

View file

@ -1,18 +1,18 @@
error[E0277]: the trait bound `String: FromRequestParts<S>` is not satisfied
--> tests/from_request/fail/parts_extracting_body.rs:6:11
--> tests/from_request/fail/parts_extracting_body.rs:5:11
|
6 | body: String,
5 | body: String,
| ^^^^^^ the trait `FromRequestParts<S>` is not implemented for `String`
|
= note: Function argument is not a valid axum extractor.
See `https://docs.rs/axum/0.7/axum/extract/index.html` for details
= help: the following other types implement trait `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>>
`()` implements `FromRequestParts<S>`
`(T1, T2)` implements `FromRequestParts<S>`
`(T1, T2, T3)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
and $N others

View file

@ -1,19 +1,39 @@
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>`:
bool
char
isize
i8
i16
i32
i64
i128
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `MyPath` to implement `serde::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 following other types implement trait `serde::de::Deserialize<'de>`:
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `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

@ -5,9 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
# 0.7.6
- None.
- **change:** Avoid cloning `Arc` during deserialization of `Path`
- **added:** `axum::serve::Serve::tcp_nodelay` and `axum::serve::WithGracefulShutdown::tcp_nodelay` ([#2653])
- **added:** `Router::has_routes` function ([#2790])
- **change:** Update tokio-tungstenite to 0.23 ([#2841])
- **added:** `Serve::local_addr` and `WithGracefulShutdown::local_addr` functions ([#2881])
[#2653]: https://github.com/tokio-rs/axum/pull/2653
[#2790]: https://github.com/tokio-rs/axum/pull/2790
[#2841]: https://github.com/tokio-rs/axum/pull/2841
[#2881]: https://github.com/tokio-rs/axum/pull/2881
# 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)
@ -546,7 +580,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
```rust
use axum::{Json, http::HeaderMap};
// This wont compile on 0.6 because both `Json` and `String` need to consume
// This won't compile on 0.6 because both `Json` and `String` need to consume
// the request body. You can use either `Json` or `String`, but not both.
async fn handler_1(
json: Json<serde_json::Value>,
@ -1128,7 +1162,7 @@ Yanked, as it didn't compile in release mode.
```rust
use axum::{Json, http::HeaderMap};
// This wont compile on 0.6 because both `Json` and `String` need to consume
// This won't compile on 0.6 because both `Json` and `String` need to consume
// the request body. You can use either `Json` or `String`, but not both.
async fn handler_1(
json: Json<serde_json::Value>,

View file

@ -1,6 +1,6 @@
[package]
name = "axum"
version = "0.7.3"
version = "0.7.6"
categories = ["asynchronous", "network-programming", "web-programming::http-server"]
description = "Web framework that focuses on ergonomics and modularity"
edition = "2021"
@ -24,8 +24,8 @@ default = [
"tracing",
]
form = ["dep:serde_urlencoded"]
http1 = ["dep:hyper", "hyper?/http1"]
http2 = ["dep:hyper", "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 = []
@ -41,7 +41,7 @@ ws = ["dep:hyper", "tokio", "dep:tokio-tungstenite", "dep:sha1", "dep:base64"]
__private_docs = ["tower/full", "dep:tower-http"]
[dependencies]
axum-core = { path = "../axum-core", version = "0.4.2" }
axum-core = { path = "../axum-core", version = "0.4.4" }
bytes = "1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0"
@ -53,28 +53,29 @@ 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"
tower = { version = "0.4.13", default-features = false, features = ["util"] }
sync_wrapper = "1.0.0"
tower = { version = "0.5.1", default-features = false, features = ["util"] }
tower-layer = "0.3.2"
tower-service = "0.3"
# optional dependencies
axum-macros = { path = "../axum-macros", version = "0.4.0", optional = true }
axum-macros = { path = "../axum-macros", version = "0.4.2", optional = true }
base64 = { version = "0.21.0", optional = true }
hyper = { version = "1.1.0", optional = true }
hyper-util = { version = "0.1.2", features = ["tokio", "server", "server-auto"], 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.21", optional = true }
tokio-tungstenite = { version = "0.23", optional = true }
tracing = { version = "0.1", default-features = false, optional = true }
[dependencies.tower-http]
version = "0.5.0"
version = "0.6.0"
optional = true
features = [
# all tower-http features except (de)?compression-zstd which doesn't
@ -108,21 +109,18 @@ features = [
"validate-request",
]
[build-dependencies]
rustversion = "1.0.9"
[dev-dependencies]
anyhow = "1.0"
axum-macros = { path = "../axum-macros", version = "0.4.0", 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.23"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["json"] }
uuid = { version = "1.0", features = ["serde", "v4"] }
@ -133,7 +131,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[dev-dependencies.tower]
package = "tower"
version = "0.4.10"
version = "0.5.1"
features = [
"util",
"timeout",
@ -144,7 +142,7 @@ features = [
]
[dev-dependencies.tower-http]
version = "0.5.0"
version = "0.6.0"
features = [
# all tower-http features except (de)?compression-zstd which doesn't
# build on `--target armv5te-unknown-linux-musleabi`

View file

@ -23,6 +23,12 @@ In particular the last point is what sets `axum` apart from other frameworks.
authorization, and more, for free. It also enables you to share middleware with
applications written using [`hyper`] or [`tonic`].
## Breaking changes
We are currently working towards axum 0.8 so the `main` branch contains breaking
changes. See the [`0.7.x`] branch for what's released to crates.io and up to
date changelogs.
## Usage example
```rust

View file

@ -198,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,7 +7,7 @@ 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};

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>;
}
@ -101,6 +103,7 @@ where
}
}
#[allow(dead_code)]
pub(crate) struct MakeErasedRouter<S> {
pub(crate) router: Router<S>,
pub(crate) into_route: fn(Router<S>, S) -> Route,
@ -118,7 +121,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

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

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:
@ -282,10 +280,15 @@ let app = Router::new().route("/users", post(create_user));
# let _: Router = app;
```
Another option is to make use of the optional extractors in [axum-extra] that
either returns `None` if there are no query parameters in the request URI,
or returns `Some(T)` if deserialization was successful.
If the deserialization was not successful, the request is rejected.
# 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
@ -699,6 +702,7 @@ 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`.
[axum-extra]: https://docs.rs/axum-extra/latest/axum_extra/extract/index.html
[`body::Body`]: crate::body::Body
[`Bytes`]: crate::body::Bytes
[customize-extractor-error]: https://github.com/tokio-rs/axum/blob/main/examples/customize-extractor-error/src/main.rs

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
@ -128,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
@ -352,11 +352,11 @@ readiness inside the response future returned by `Service::call`. This works
well when your services don't care about backpressure and are always ready
anyway.
axum expects that all services used in your app wont care about
axum expects that all services used in your app won't care about
backpressure and so it uses the latter strategy. However that means you
should avoid routing to a service (or using a middleware) that _does_ care
about backpressure. At the very least you should [load shed] so requests are
dropped quickly and don't keep piling up.
about backpressure. At the very least you should [load shed][tower::load_shed]
so requests are dropped quickly and don't keep piling up.
It also means that if `poll_ready` returns an error then that error will be
returned in the response future from `call` and _not_ from `poll_ready`. In
@ -388,8 +388,7 @@ let app = ServiceBuilder::new()
```
However when applying middleware around your whole application in this way
you have to take care that errors are still being handled with
appropriately.
you have to take care that errors are still being handled appropriately.
Also note that handlers created from async functions don't care about
backpressure and are always ready. So if you're not using any Tower

View file

@ -166,7 +166,7 @@ In general you can return tuples like:
This means you cannot accidentally override the status or body as [`IntoResponseParts`] only allows
setting headers and extensions.
Use [`Response`](crate::response::Response) for more low level control:
Use [`Response`] for more low level control:
```rust,no_run
use axum::{

View file

@ -181,7 +181,7 @@ router.
# Panics
- If the route overlaps with another route. See [`Router::route`]
for more details.
for more details.
- If the route contains a wildcard (`*`).
- If `path` is empty.

View file

@ -5,8 +5,7 @@ can be either static, a capture, or a wildcard.
`method_router` is the [`MethodRouter`] that should receive the request if the
path matches `path`. `method_router` will commonly be a handler wrapped in a method
router like [`get`](crate::routing::get). See [`handler`](crate::handler) for
more details on handlers.
router like [`get`]. See [`handler`](crate::handler) for more details on handlers.
# Static paths
@ -56,7 +55,22 @@ Note that `/*key` doesn't match empty segments. Thus:
- `/*key` doesn't match `/` but does match `/a`, `/a/`, etc.
- `/x/*key` doesn't match `/x` or `/x/` but does match `/x/a`, `/x/a/`, etc.
Wildcard captures can also be extracted using [`Path`](crate::extract::Path).
Wildcard captures can also be extracted using [`Path`](crate::extract::Path):
```rust
use axum::{
Router,
routing::get,
extract::Path,
};
let app: Router = Router::new().route("/*key", get(handler));
async fn handler(Path(path): Path<String>) -> String {
path
}
```
Note that the leading slash is not included, i.e. for the route `/foo/*rest` and
the path `/foo/bar/baz` the value of `rest` will be `bar/baz`.

View file

@ -11,6 +11,10 @@ the request matches a route. This is useful for middleware that return early
(such as authorization) which might otherwise convert a `404 Not Found` into a
`401 Unauthorized`.
This function will panic if no routes have been declared yet on the router,
since the new layer will have no effect, and this is typically a bug.
In generic code, you can test if that is the case first, by calling [`Router::has_routes`].
# Example
```rust

View file

@ -20,7 +20,7 @@ axum::serve(listener, routes).await.unwrap();
# Returning routers with states from functions
When returning `Router`s from functions it is generally recommend not set the
When returning `Router`s from functions, it is generally recommended not to set the
state directly:
```rust
@ -171,7 +171,7 @@ work:
# #[derive(Clone)]
# struct AppState {}
#
// This wont work because we're returning a `Router<AppState>`
// This won't work because we're returning a `Router<AppState>`
// i.e. we're saying we're still missing an `AppState`
fn routes(state: AppState) -> Router<AppState> {
Router::new()

View file

@ -85,8 +85,7 @@ where
"Extension of type `{}` was not found. Perhaps you forgot to add it? See `axum::Extension`.",
std::any::type_name::<T>()
))
})
.map(|x| x.clone())?;
}).cloned()?;
Ok(Extension(value))
}

View file

@ -223,7 +223,6 @@ where
mod tests {
use super::*;
use crate::{routing::get, serve::IncomingStream, test_helpers::TestClient, Router};
use std::net::SocketAddr;
use tokio::net::TcpListener;
#[crate::test]
@ -309,7 +308,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/").send().await;
let res = client.get("/").await;
let body = res.text().await;
assert!(body.starts_with("0.0.0.0:1337"));
}

View file

@ -94,7 +94,6 @@ mod tests {
let host = test_client()
.get("/")
.header(http::header::HOST, original_host)
.send()
.await
.text()
.await;
@ -107,7 +106,6 @@ mod tests {
let host = test_client()
.get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, original_host)
.send()
.await
.text()
.await;
@ -122,7 +120,6 @@ mod tests {
.get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header)
.header(http::header::HOST, host_header)
.send()
.await
.text()
.await;
@ -131,7 +128,7 @@ mod tests {
#[crate::test]
async fn uri_host() {
let host = test_client().get("/").send().await.text().await;
let host = test_client().get("/").await.text().await;
assert!(host.contains("127.0.0.1"));
}

View file

@ -147,7 +147,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
let res = client.get("/foo").await;
assert_eq!(res.text().await, "/:a");
}
@ -163,7 +163,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.text().await, "/:a/:b");
}
@ -182,7 +182,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar/baz").send().await;
let res = client.get("/foo/bar/baz").await;
assert_eq!(res.text().await, "/:a/:b/:c");
}
@ -202,7 +202,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -222,7 +222,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -239,7 +239,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -256,7 +256,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -276,7 +276,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -297,7 +297,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -311,7 +311,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -331,7 +331,7 @@ 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);
}
@ -346,7 +346,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
}

View file

@ -104,7 +104,7 @@ mod tests {
let app = Router::new().route("/", get(|body: String| async { body }));
let client = TestClient::new(app);
let res = client.get("/").body("foo").send().await;
let res = client.get("/").body("foo").await;
let body = res.text().await;
assert_eq!(body, "foo");

View file

@ -272,12 +272,13 @@ impl std::error::Error for MultipartError {
impl IntoResponse for MultipartError {
fn into_response(self) -> Response {
let body = self.body_text();
axum_core::__log_rejection!(
rejection_type = Self,
body_text = self.body_text(),
body_text = body,
status = self.status(),
);
(self.status(), self.body_text()).into_response()
(self.status(), body).into_response()
}
}
@ -308,7 +309,7 @@ mod tests {
use axum_core::extract::DefaultBodyLimit;
use super::*;
use crate::{response::IntoResponse, routing::post, test_helpers::*, Router};
use crate::{routing::post, test_helpers::*, Router};
#[crate::test]
async fn content_type_with_encoding() {
@ -343,7 +344,7 @@ mod tests {
)])),
);
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
@ -374,7 +375,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

@ -133,7 +133,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
let res = client.get("/api/users").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -151,7 +151,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
let res = client.get("/api/users").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -169,7 +169,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/api/v2/users").send().await;
let res = client.get("/api/v2/users").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -187,7 +187,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/api/v2/users").send().await;
let res = client.get("/api/v2/users").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -205,7 +205,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/users").send().await;
let res = client.get("/users").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -223,7 +223,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
let res = client.get("/api/users").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -238,7 +238,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/api/doesnt-exist").send().await;
let res = client.get("/api/doesnt-exist").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -257,7 +257,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
let res = client.get("/api/users").await;
assert_eq!(res.status(), StatusCode::OK);
}
}

View file

@ -210,14 +210,14 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
}
visitor.visit_enum(EnumDeserializer {
value: self.url_params[0].1.clone().into_inner(),
value: &self.url_params[0].1,
})
}
}
struct MapDeserializer<'de> {
params: &'de [(Arc<str>, PercentDecodedStr)],
key: Option<KeyOrIdx>,
key: Option<KeyOrIdx<'de>>,
value: Option<&'de PercentDecodedStr>,
}
@ -232,11 +232,8 @@ impl<'de> MapAccess<'de> for MapDeserializer<'de> {
Some(((key, value), tail)) => {
self.value = Some(value);
self.params = tail;
self.key = Some(KeyOrIdx::Key(key.clone()));
seed.deserialize(KeyDeserializer {
key: Arc::clone(key),
})
.map(Some)
self.key = Some(KeyOrIdx::Key(key));
seed.deserialize(KeyDeserializer { key }).map(Some)
}
None => Ok(None),
}
@ -256,8 +253,8 @@ impl<'de> MapAccess<'de> for MapDeserializer<'de> {
}
}
struct KeyDeserializer {
key: Arc<str>,
struct KeyDeserializer<'de> {
key: &'de str,
}
macro_rules! parse_key {
@ -271,7 +268,7 @@ macro_rules! parse_key {
};
}
impl<'de> Deserializer<'de> for KeyDeserializer {
impl<'de> Deserializer<'de> for KeyDeserializer<'de> {
type Error = PathDeserializationError;
parse_key!(deserialize_identifier);
@ -302,7 +299,7 @@ macro_rules! parse_value {
if let Some(key) = self.key.take() {
let kind = match key {
KeyOrIdx::Key(key) => ErrorKind::ParseErrorAtKey {
key: key.to_string(),
key: key.to_owned(),
value: self.value.as_str().to_owned(),
expected_type: $ty,
},
@ -327,7 +324,7 @@ macro_rules! parse_value {
#[derive(Debug)]
struct ValueDeserializer<'de> {
key: Option<KeyOrIdx>,
key: Option<KeyOrIdx<'de>>,
value: &'de PercentDecodedStr,
}
@ -416,7 +413,7 @@ impl<'de> Deserializer<'de> for ValueDeserializer<'de> {
V: Visitor<'de>,
{
struct PairDeserializer<'de> {
key: Option<KeyOrIdx>,
key: Option<KeyOrIdx<'de>>,
value: Option<&'de PercentDecodedStr>,
}
@ -507,9 +504,7 @@ impl<'de> Deserializer<'de> for ValueDeserializer<'de> {
where
V: Visitor<'de>,
{
visitor.visit_enum(EnumDeserializer {
value: self.value.clone().into_inner(),
})
visitor.visit_enum(EnumDeserializer { value: self.value })
}
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -520,11 +515,11 @@ impl<'de> Deserializer<'de> for ValueDeserializer<'de> {
}
}
struct EnumDeserializer {
value: Arc<str>,
struct EnumDeserializer<'de> {
value: &'de str,
}
impl<'de> EnumAccess<'de> for EnumDeserializer {
impl<'de> EnumAccess<'de> for EnumDeserializer<'de> {
type Error = PathDeserializationError;
type Variant = UnitVariant;
@ -598,10 +593,7 @@ impl<'de> SeqAccess<'de> for SeqDeserializer<'de> {
let idx = self.idx;
self.idx += 1;
Ok(Some(seed.deserialize(ValueDeserializer {
key: Some(KeyOrIdx::Idx {
idx,
key: key.clone(),
}),
key: Some(KeyOrIdx::Idx { idx, key }),
value,
})?))
}
@ -611,9 +603,9 @@ impl<'de> SeqAccess<'de> for SeqDeserializer<'de> {
}
#[derive(Debug, Clone)]
enum KeyOrIdx {
Key(Arc<str>),
Idx { idx: usize, key: Arc<str> },
enum KeyOrIdx<'de> {
Key(&'de str),
Idx { idx: usize, key: &'de str },
}
#[cfg(test)]

View file

@ -396,12 +396,13 @@ impl FailedToDeserializePathParams {
impl IntoResponse for FailedToDeserializePathParams {
fn into_response(self) -> Response {
let body = self.body_text();
axum_core::__log_rejection!(
rejection_type = Self,
body_text = self.body_text(),
body_text = body,
status = self.status(),
);
(self.status(), self.body_text()).into_response()
(self.status(), body).into_response()
}
}
@ -527,7 +528,13 @@ impl std::error::Error for InvalidUtf8InPathParam {}
impl IntoResponse for InvalidUtf8InPathParam {
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()
}
}
@ -535,7 +542,6 @@ impl IntoResponse for InvalidUtf8InPathParam {
mod tests {
use super::*;
use crate::{routing::get, test_helpers::*, Router};
use http::StatusCode;
use serde::Deserialize;
use std::collections::HashMap;
@ -553,10 +559,10 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/users/42").send().await;
let res = client.get("/users/42").await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.post("/users/1337").send().await;
let res = client.post("/users/1337").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -566,7 +572,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/users/42").send().await;
let res = client.get("/users/42").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -579,7 +585,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/one%20two").send().await;
let res = client.get("/one%20two").await;
assert_eq!(res.text().await, "one two");
}
@ -598,10 +604,10 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/i/123").send().await;
let res = client.get("/i/123").await;
assert_eq!(res.text().await, "123");
let res = client.get("/u/123").send().await;
let res = client.get("/u/123").await;
assert_eq!(res.text().await, "123");
}
@ -621,10 +627,10 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar/baz").send().await;
let res = client.get("/foo/bar/baz").await;
assert_eq!(res.text().await, "bar/baz");
let res = client.get("/bar/baz/qux").send().await;
let res = client.get("/bar/baz/qux").await;
assert_eq!(res.text().await, "baz/qux");
}
@ -634,10 +640,10 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/").send().await;
let res = client.get("/").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res = client.get("/foo").send().await;
let res = client.get("/foo").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -650,10 +656,10 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/abc/method").send().await;
let res = client.get("/abc/method").await;
assert_eq!(res.text().await, "abc");
let res = client.get("//method").send().await;
let res = client.get("//method").await;
assert_eq!(res.text().await, "");
}
@ -666,13 +672,13 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/method/abc").send().await;
let res = client.get("/method/abc").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res = client.get("/method/abc/").send().await;
let res = client.get("/method/abc/").await;
assert_eq!(res.text().await, "abc");
let res = client.get("/method//").send().await;
let res = client.get("/method//").await;
assert_eq!(res.text().await, "");
}
@ -685,16 +691,16 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/method/abc/").send().await;
let res = client.get("/method/abc/").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res = client.get("/method/abc").send().await;
let res = client.get("/method/abc").await;
assert_eq!(res.text().await, "abc");
let res = client.get("/method/").send().await;
let res = client.get("/method/").await;
assert_eq!(res.text().await, "");
let res = client.get("/method").send().await;
let res = client.get("/method").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
@ -715,11 +721,11 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
let res = client.get("/foo").await;
assert_eq!(res.text().await, "foo");
// percent decoding should also work
let res = client.get("/foo%20bar").send().await;
let res = client.get("/foo%20bar").await;
assert_eq!(res.text().await, "foo bar");
}
@ -729,7 +735,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/a/b").send().await;
let res = client.get("/a/b").await;
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
res.text().await,
@ -755,7 +761,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").send().await;
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
@ -813,40 +819,27 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/single/2023-01-01").send().await;
let res = client.get("/single/2023-01-01").await;
assert_eq!(res.text().await, "single: 2023-01-01");
let res = client
.get("/tuple/2023-01-01/2023-01-02/2023-01-03")
.send()
.await;
let res = client.get("/tuple/2023-01-01/2023-01-02/2023-01-03").await;
assert_eq!(res.text().await, "tuple: 2023-01-01 2023-01-02 2023-01-03");
let res = client
.get("/vec/2023-01-01/2023-01-02/2023-01-03")
.send()
.await;
let res = client.get("/vec/2023-01-01/2023-01-02/2023-01-03").await;
assert_eq!(res.text().await, "vec: 2023-01-01 2023-01-02 2023-01-03");
let res = client
.get("/vec_pairs/2023-01-01/2023-01-02/2023-01-03")
.send()
.await;
assert_eq!(
res.text().await,
"vec_pairs: 2023-01-01 2023-01-02 2023-01-03",
);
let res = client
.get("/map/2023-01-01/2023-01-02/2023-01-03")
.send()
.await;
let res = client.get("/map/2023-01-01/2023-01-02/2023-01-03").await;
assert_eq!(res.text().await, "map: 2023-01-01 2023-01-02 2023-01-03");
let res = client
.get("/struct/2023-01-01/2023-01-02/2023-01-03")
.send()
.await;
let res = client.get("/struct/2023-01-01/2023-01-02/2023-01-03").await;
assert_eq!(res.text().await, "struct: 2023-01-01 2023-01-02 2023-01-03");
}
@ -860,13 +853,13 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/one/1").send().await;
let res = client.get("/one/1").await;
assert!(res
.text()
.await
.starts_with("Wrong number of path arguments for `Path`. Expected 2 but got 1"));
let res = client.get("/two/1/2").send().await;
let res = client.get("/two/1/2").await;
assert!(res
.text()
.await
@ -887,7 +880,7 @@ mod tests {
);
let client = TestClient::new(app);
let res = client.get("/foo/bar/baz").send().await;
let res = client.get("/foo/bar/baz").await;
let body = res.text().await;
assert_eq!(body, "a=foo b=bar c=baz");
}

Some files were not shown because too many files have changed in this diff Show more