From 8d00a14736bd9a6d977a0087ad2c5f249dddd06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Mon, 31 Mar 2025 15:44:32 +0200 Subject: [PATCH 01/10] axum-extra: Remove unused feature --- axum-extra/CHANGELOG.md | 6 ++++++ axum-extra/Cargo.toml | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 23edf95763..3d4747cb08 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning]. +- **breaking:** Remove unused `async-stream` feature, which was accidentally + introduced as an implicit feature through an optional dependency which was no + longer being used ([#3298]) + +[#3298]: https://github.com/tokio-rs/axum/pull/3298 + # 0.11.0 Yanked from crates.io due to unforeseen breaking change, see [#3190] for details. diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index da09715e26..8b9c7629b4 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -15,7 +15,6 @@ version = "0.10.3" default = ["tracing"] async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"] -async-stream = [] # unused, remove before the next breaking-change release file-stream = ["dep:tokio-util", "tokio-util?/io", "dep:tokio", "tokio?/fs", "tokio?/io-util"] attachment = ["dep:tracing"] error-response = ["dep:tracing", "tracing/std"] From 69703479c810240b21ecacffacd4817fd9eb2767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Fri, 12 Sep 2025 14:33:48 +0200 Subject: [PATCH 02/10] axum-extra: make `option_layer` guarantee that the output body is `axum::body::Body` --- axum-extra/CHANGELOG.md | 1 + axum-extra/src/middleware.rs | 62 +++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 3d4747cb08..f15817bf2c 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning]. - **breaking:** Remove unused `async-stream` feature, which was accidentally introduced as an implicit feature through an optional dependency which was no longer being used ([#3298]) +- **breaking:** `option_layer` now maps the `Response` body type to `axum::body::Body` ([#3469]) [#3298]: https://github.com/tokio-rs/axum/pull/3298 diff --git a/axum-extra/src/middleware.rs b/axum-extra/src/middleware.rs index 0303d484fd..2407a16643 100644 --- a/axum-extra/src/middleware.rs +++ b/axum-extra/src/middleware.rs @@ -1,6 +1,7 @@ //! Additional middleware utilities. use crate::either::Either; +use axum::middleware::ResponseAxumBodyLayer; use tower_layer::Identity; /// Convert an `Option` into a [`Layer`]. @@ -26,19 +27,58 @@ use tower_layer::Identity; /// /// # Difference between this and [`tower::util::option_layer`] /// -/// [`tower::util::option_layer`] always changes the error type to [`BoxError`] which requires -/// using [`HandleErrorLayer`] when used with axum, even if the layer you're applying uses -/// [`Infallible`]. -/// -/// `axum_extra::middleware::option_layer` on the other hand doesn't change the error type so can -/// be applied directly. +/// `axum_extra::middleware::option_layer` makes sure that the output `Body` is [`axum::body::Body`]. /// /// [`Layer`]: tower_layer::Layer -/// [`BoxError`]: tower::BoxError -/// [`HandleErrorLayer`]: axum::error_handling::HandleErrorLayer -/// [`Infallible`]: std::convert::Infallible -pub fn option_layer(layer: Option) -> Either { +pub fn option_layer(layer: Option) -> Either<(ResponseAxumBodyLayer, L), Identity> { layer - .map(Either::E1) + .map(|layer| Either::E1((ResponseAxumBodyLayer, layer))) .unwrap_or_else(|| Either::E2(Identity::new())) } + +#[cfg(test)] +mod tests { + use std::{ + convert::Infallible, + pin::Pin, + task::{Context, Poll}, + }; + + use axum::{body::Body as AxumBody, Router}; + use bytes::Bytes; + use http_body::Body as HttpBody; + use tower_http::map_response_body::MapResponseBodyLayer; + + use super::option_layer; + + #[test] + fn remap_response_body() { + struct BodyWrapper; + + impl BodyWrapper { + fn new(_: AxumBody) -> Self { + Self + } + } + + impl HttpBody for BodyWrapper { + type Data = Bytes; + type Error = Infallible; + fn poll_frame( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + unimplemented!() + } + fn is_end_stream(&self) -> bool { + unimplemented!() + } + fn size_hint(&self) -> http_body::SizeHint { + unimplemented!() + } + } + let _app: Router = Router::new().layer(option_layer(Some(MapResponseBodyLayer::new( + BodyWrapper::new, + )))); + } +} From 929512f46225aac68255dd9f7eb4827b4b8b83f9 Mon Sep 17 00:00:00 2001 From: tottoto Date: Sun, 21 Sep 2025 05:50:56 +0900 Subject: [PATCH 03/10] axum-extra: Make axum optional dependency (#3485) --- Cargo.lock | 1 + axum-extra/CHANGELOG.md | 8 +++ axum-extra/Cargo.toml | 62 +++++++++---------- axum-extra/src/body/async_read_body.rs | 6 +- axum-extra/src/either.rs | 2 +- axum-extra/src/extract/cookie/mod.rs | 2 +- axum-extra/src/extract/cookie/private.rs | 2 +- axum-extra/src/extract/cookie/signed.rs | 2 +- axum-extra/src/extract/host.rs | 2 +- axum-extra/src/extract/json_deserializer.rs | 2 +- axum-extra/src/extract/mod.rs | 20 +++++- axum-extra/src/extract/multipart.rs | 14 ++--- axum-extra/src/extract/query.rs | 2 +- axum-extra/src/extract/scheme.rs | 3 +- axum-extra/src/json_lines.rs | 10 +-- axum-extra/src/lib.rs | 16 ++++- axum-extra/src/protobuf.rs | 6 +- axum-extra/src/response/attachment.rs | 4 +- axum-extra/src/response/erased_json.rs | 6 +- axum-extra/src/response/file_stream.rs | 2 +- axum-extra/src/response/mod.rs | 6 +- axum-extra/src/response/multiple.rs | 2 +- axum-extra/src/typed_header.rs | 2 +- axum-macros/Cargo.toml | 2 +- examples/customize-extractor-error/Cargo.toml | 2 +- 25 files changed, 109 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1178a79117..51d5bbd078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "cookie", "fastrand", "form_urlencoded", + "futures-core", "futures-util", "headers", "http 1.3.1", diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index f15817bf2c..6d935ebab5 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -9,8 +9,16 @@ and this project adheres to [Semantic Versioning]. introduced as an implicit feature through an optional dependency which was no longer being used ([#3298]) - **breaking:** `option_layer` now maps the `Response` body type to `axum::body::Body` ([#3469]) +- **breaking:** Some new features are added which need to be opted in ([#3485]). + - `Cached` extractor requires `cached` feature. + - The handler utilities require `handler` feature. + - The middleware utilities require `middleware` feature. + - `OptionalPath` extractor requires `optional-path` feature. + - The routing utilities require `routing` feature. + - `WithRejection` extractor requires `with-rejection` feature. [#3298]: https://github.com/tokio-rs/axum/pull/3298 +[#3485]: https://github.com/tokio-rs/axum/pull/3485 # 0.11.0 diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 8b9c7629b4..807e36a67a 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -15,7 +15,14 @@ version = "0.10.3" default = ["tracing"] async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"] -file-stream = ["dep:tokio-util", "tokio-util?/io", "dep:tokio", "tokio?/fs", "tokio?/io-util"] +cached = ["dep:axum"] +file-stream = [ + "dep:tokio-util", + "tokio-util?/io", + "dep:tokio", + "tokio?/fs", + "tokio?/io-util", +] attachment = ["dep:tracing"] error-response = ["dep:tracing", "tracing/std"] cookie = ["dep:cookie"] @@ -23,7 +30,13 @@ cookie-private = ["cookie", "cookie?/private"] cookie-signed = ["cookie", "cookie?/signed"] cookie-key-expansion = ["cookie", "cookie?/key-expansion"] erased-json = ["dep:serde_json", "dep:typed-json"] -form = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"] +form = [ + "dep:axum", + "dep:form_urlencoded", + "dep:serde_html_form", + "dep:serde_path_to_error", +] +handler = ["dep:axum"] json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"] json-lines = [ "dep:serde_json", @@ -33,22 +46,31 @@ json-lines = [ "tokio-stream?/io-util", "dep:tokio", ] +middleware = ["dep:axum"] multipart = ["dep:multer", "dep:fastrand"] +optional-path = ["dep:axum"] protobuf = ["dep:prost"] +routing = ["axum/original-uri"] scheme = [] query = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"] tracing = ["axum-core/tracing", "axum/tracing", "dep:tracing"] typed-header = ["dep:headers"] -typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"] +typed-routing = [ + "dep:axum-macros", + "dep:percent-encoding", + "dep:serde_html_form", + "dep:form_urlencoded", +] +with-rejection = ["dep:axum"] # Enabled by docs.rs because it uses all-features # Enables upstream things linked to in docs -__private_docs = ["axum/json", "dep:serde", "dep:tower"] +__private_docs = ["axum/json", "dep:serde"] [dependencies] -axum = { path = "../axum", version = "0.8.6", default-features = false, features = ["original-uri"] } -axum-core = { path = "../axum-core", version = "0.5.5" } +axum-core = { path = "../axum-core", version = "0.5.2" } bytes = "1.1.0" +futures-core = "0.3" futures-util = { version = "0.3", default-features = false, features = ["alloc"] } http = "1.0.0" http-body = "1.0.0" @@ -57,10 +79,12 @@ mime = "0.3" pin-project-lite = "0.2" rustversion = "1.0.9" serde_core = "1.0.221" +tower = { version = "0.5.2", default-features = false, features = ["util"] } tower-layer = "0.3" tower-service = "0.3" # optional dependencies +axum = { path = "../axum", version = "0.8.4", default-features = false, optional = true } axum-macros = { path = "../axum-macros", version = "0.5.0", optional = true } cookie = { package = "cookie", version = "0.18.0", features = ["percent-encode"], optional = true } fastrand = { version = "2.1.0", optional = true } @@ -80,7 +104,6 @@ typed-json = { version = "0.1.1", optional = true } # doc dependencies serde = { version = "1.0.221", optional = true } -tower = { version = "0.5.2", default-features = false, features = ["util"], optional = true } [dev-dependencies] axum = { path = "../axum", features = ["macros", "__private"] } @@ -96,28 +119,3 @@ tracing-subscriber = "0.3.19" [lints] workspace = true - -[package.metadata.docs.rs] -all-features = true - -[package.metadata.cargo-public-api-crates] -allowed = [ - "axum", - "axum_core", - "axum_macros", - "bytes", - "cookie", - "futures_core", - "futures_util", - "headers", - "headers_core", - "http", - "http_body", - "pin_project_lite", - "prost", - "serde_core", - "tokio", - "tokio_util", - "tower_layer", - "tower_service", -] diff --git a/axum-extra/src/body/async_read_body.rs b/axum-extra/src/body/async_read_body.rs index ec273b0549..972965dcc5 100644 --- a/axum-extra/src/body/async_read_body.rs +++ b/axum-extra/src/body/async_read_body.rs @@ -1,8 +1,10 @@ -use axum::{ - body::{Body, Bytes, HttpBody}, +use axum_core::{ + body::Body, response::{IntoResponse, Response}, Error, }; +use bytes::Bytes; +use http_body::Body as HttpBody; use pin_project_lite::pin_project; use std::{ pin::Pin, diff --git a/axum-extra/src/either.rs b/axum-extra/src/either.rs index 5d0e33eb7c..1e4ad53e94 100755 --- a/axum-extra/src/either.rs +++ b/axum-extra/src/either.rs @@ -92,7 +92,7 @@ use std::task::{Context, Poll}; -use axum::{ +use axum_core::{ extract::FromRequestParts, response::{IntoResponse, Response}, }; diff --git a/axum-extra/src/extract/cookie/mod.rs b/axum-extra/src/extract/cookie/mod.rs index 5bd110b852..a02b3f614f 100644 --- a/axum-extra/src/extract/cookie/mod.rs +++ b/axum-extra/src/extract/cookie/mod.rs @@ -2,7 +2,7 @@ //! //! See [`CookieJar`], [`SignedCookieJar`], and [`PrivateCookieJar`] for more details. -use axum::{ +use axum_core::{ extract::FromRequestParts, response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, }; diff --git a/axum-extra/src/extract/cookie/private.rs b/axum-extra/src/extract/cookie/private.rs index 85f21f8583..f6f885b95f 100644 --- a/axum-extra/src/extract/cookie/private.rs +++ b/axum-extra/src/extract/cookie/private.rs @@ -1,5 +1,5 @@ use super::{cookies_from_request, set_cookies, Cookie, Key}; -use axum::{ +use axum_core::{ extract::{FromRef, FromRequestParts}, response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, }; diff --git a/axum-extra/src/extract/cookie/signed.rs b/axum-extra/src/extract/cookie/signed.rs index f0e07e7573..dbcf102b3a 100644 --- a/axum-extra/src/extract/cookie/signed.rs +++ b/axum-extra/src/extract/cookie/signed.rs @@ -1,5 +1,5 @@ use super::{cookies_from_request, set_cookies}; -use axum::{ +use axum_core::{ extract::{FromRef, FromRequestParts}, response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, }; diff --git a/axum-extra/src/extract/host.rs b/axum-extra/src/extract/host.rs index e9eb91c5be..f42018d219 100644 --- a/axum-extra/src/extract/host.rs +++ b/axum-extra/src/extract/host.rs @@ -1,5 +1,5 @@ use super::rejection::{FailedToResolveHost, HostRejection}; -use axum::{ +use axum_core::{ extract::{FromRequestParts, OptionalFromRequestParts}, RequestPartsExt, }; diff --git a/axum-extra/src/extract/json_deserializer.rs b/axum-extra/src/extract/json_deserializer.rs index 6ad11ed934..16737c21eb 100644 --- a/axum-extra/src/extract/json_deserializer.rs +++ b/axum-extra/src/extract/json_deserializer.rs @@ -1,7 +1,7 @@ -use axum::extract::{FromRequest, Request}; use axum_core::__composite_rejection as composite_rejection; use axum_core::__define_rejection as define_rejection; use axum_core::extract::rejection::BytesRejection; +use axum_core::extract::{FromRequest, Request}; use bytes::Bytes; use http::{header, HeaderMap}; use serde_core::Deserialize; diff --git a/axum-extra/src/extract/mod.rs b/axum-extra/src/extract/mod.rs index f14460f712..cd55ecf294 100644 --- a/axum-extra/src/extract/mod.rs +++ b/axum-extra/src/extract/mod.rs @@ -1,9 +1,15 @@ //! Additional extractors. -mod cached; mod host; -mod optional_path; pub mod rejection; + +#[cfg(feature = "optional-path")] +mod optional_path; + +#[cfg(feature = "cached")] +mod cached; + +#[cfg(feature = "with-rejection")] mod with_rejection; #[cfg(feature = "form")] @@ -25,8 +31,16 @@ pub mod multipart; mod scheme; #[allow(deprecated)] +#[cfg(feature = "optional-path")] pub use self::optional_path::OptionalPath; -pub use self::{cached::Cached, host::Host, with_rejection::WithRejection}; + +pub use self::host::Host; + +#[cfg(feature = "cached")] +pub use self::cached::Cached; + +#[cfg(feature = "with-rejection")] +pub use self::with_rejection::WithRejection; #[cfg(feature = "cookie")] pub use self::cookie::CookieJar; diff --git a/axum-extra/src/extract/multipart.rs b/axum-extra/src/extract/multipart.rs index cc2e38cbfc..b3d86c4c2f 100644 --- a/axum-extra/src/extract/multipart.rs +++ b/axum-extra/src/extract/multipart.rs @@ -2,15 +2,15 @@ //! //! See [`Multipart`] for more details. -use axum::{ - body::{Body, Bytes}, +use axum_core::{ + RequestExt, __composite_rejection as composite_rejection, + __define_rejection as define_rejection, + body::Body, extract::FromRequest, response::{IntoResponse, Response}, - RequestExt, }; -use axum_core::__composite_rejection as composite_rejection; -use axum_core::__define_rejection as define_rejection; -use futures_util::stream::Stream; +use bytes::Bytes; +use futures_core::stream::Stream; use http::{ header::{HeaderMap, CONTENT_TYPE}, Request, StatusCode, @@ -284,7 +284,7 @@ fn status_code_from_multer_error(err: &multer::Error) -> StatusCode { } if err - .downcast_ref::() + .downcast_ref::() .and_then(|err| err.source()) .and_then(|err| err.downcast_ref::()) .is_some() diff --git a/axum-extra/src/extract/query.rs b/axum-extra/src/extract/query.rs index 0824959840..625c583f03 100644 --- a/axum-extra/src/extract/query.rs +++ b/axum-extra/src/extract/query.rs @@ -1,6 +1,6 @@ -use axum::extract::FromRequestParts; use axum_core::__composite_rejection as composite_rejection; use axum_core::__define_rejection as define_rejection; +use axum_core::extract::FromRequestParts; use http::{request::Parts, Uri}; use serde_core::de::DeserializeOwned; diff --git a/axum-extra/src/extract/scheme.rs b/axum-extra/src/extract/scheme.rs index b20e9cf205..9d3bc0c37f 100644 --- a/axum-extra/src/extract/scheme.rs +++ b/axum-extra/src/extract/scheme.rs @@ -1,8 +1,7 @@ //! Extractor that parses the scheme of a request. //! See [`Scheme`] for more details. -use axum::extract::FromRequestParts; -use axum_core::__define_rejection as define_rejection; +use axum_core::{__define_rejection as define_rejection, extract::FromRequestParts}; use http::{ header::{HeaderMap, FORWARDED}, request::Parts, diff --git a/axum-extra/src/json_lines.rs b/axum-extra/src/json_lines.rs index 281e176316..d9afb04437 100644 --- a/axum-extra/src/json_lines.rs +++ b/axum-extra/src/json_lines.rs @@ -1,6 +1,6 @@ //! Newline delimited JSON extractor and response. -use axum::{ +use axum_core::{ body::Body, extract::{FromRequest, Request}, response::{IntoResponse, Response}, @@ -73,7 +73,7 @@ pin_project! { }, Extractor { #[pin] - stream: BoxStream<'static, Result>, + stream: BoxStream<'static, Result>, }, } } @@ -116,9 +116,9 @@ where let deserialized_stream = lines_stream - .map_err(axum::Error::new) + .map_err(axum_core::Error::new) .and_then(|value| async move { - serde_json::from_str::(&value).map_err(axum::Error::new) + serde_json::from_str::(&value).map_err(axum_core::Error::new) }); Ok(Self { @@ -131,7 +131,7 @@ where } impl Stream for JsonLines { - type Item = Result; + type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.project().inner.project() { diff --git a/axum-extra/src/lib.rs b/axum-extra/src/lib.rs index ddf21d19a0..743a926870 100644 --- a/axum-extra/src/lib.rs +++ b/axum-extra/src/lib.rs @@ -11,6 +11,7 @@ //! ---|---|--- //! `async-read-body` | Enables the [`AsyncReadBody`](crate::body::AsyncReadBody) body | No //! `attachment` | Enables the [`Attachment`](crate::response::Attachment) response | No +//! `cached` | Enables the [`Cached`](crate::extract::Cached) extractor | No //! `cookie` | Enables the [`CookieJar`](crate::extract::CookieJar) extractor | No //! `cookie-private` | Enables the [`PrivateCookieJar`](crate::extract::PrivateCookieJar) extractor | No //! `cookie-signed` | Enables the [`SignedCookieJar`](crate::extract::SignedCookieJar) extractor | No @@ -18,15 +19,20 @@ //! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response | No //! `error-response` | Enables the [`InternalServerError`](crate::response::InternalServerError) response | No //! `form` | Enables the [`Form`](crate::extract::Form) extractor | No +//! `handler` | Enables the [handler] utilities | No //! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor | No //! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response | No +//! `middleware` | Enables the [middleware] utilities | No //! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor | No +//! `optional-path` | Enables the [`OptionalPath`](crate::extract::OptionalPath) extractor | No //! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response | No //! `query` | Enables the [`Query`](crate::extract::Query) extractor | No +//! `routing` | Enables the [routing] utilities | No //! `tracing` | Log rejections from built-in extractors | Yes //! `typed-routing` | Enables the [`TypedPath`](crate::routing::TypedPath) routing utilities | No //! `typed-header` | Enables the [`TypedHeader`] extractor and response | No //! `file-stream` | Enables the [`FileStream`](crate::response::FileStream) response | No +//! `with-rejection` | Enables the [`WithRejection`](crate::extract::WithRejection) extractor | No //! //! [`axum`]: https://crates.io/crates/axum @@ -40,11 +46,17 @@ extern crate self as axum_extra; pub mod body; pub mod either; pub mod extract; -pub mod handler; -pub mod middleware; pub mod response; + +#[cfg(feature = "routing")] pub mod routing; +#[cfg(feature = "middleware")] +pub mod middleware; + +#[cfg(feature = "handler")] +pub mod handler; + #[cfg(feature = "json-lines")] pub mod json_lines; diff --git a/axum-extra/src/protobuf.rs b/axum-extra/src/protobuf.rs index fb63c7a41c..cc68af2d09 100644 --- a/axum-extra/src/protobuf.rs +++ b/axum-extra/src/protobuf.rs @@ -1,12 +1,12 @@ //! Protocol Buffer extractor and response. -use axum::{ +use axum_core::__composite_rejection as composite_rejection; +use axum_core::__define_rejection as define_rejection; +use axum_core::{ extract::{rejection::BytesRejection, FromRequest, Request}, response::{IntoResponse, Response}, RequestExt, }; -use axum_core::__composite_rejection as composite_rejection; -use axum_core::__define_rejection as define_rejection; use bytes::BytesMut; use http::StatusCode; use http_body_util::BodyExt; diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index 2063d30f05..b2af0aede1 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -1,4 +1,4 @@ -use axum::response::IntoResponse; +use axum_core::response::IntoResponse; use http::{header, HeaderMap, HeaderValue}; use tracing::error; @@ -79,7 +79,7 @@ impl IntoResponse for Attachment where T: IntoResponse, { - fn into_response(self) -> axum::response::Response { + fn into_response(self) -> axum_core::response::Response { let mut headers = HeaderMap::new(); if let Some(content_type) = self.content_type { diff --git a/axum-extra/src/response/erased_json.rs b/axum-extra/src/response/erased_json.rs index de0fc2213e..76c7390ea4 100644 --- a/axum-extra/src/response/erased_json.rs +++ b/axum-extra/src/response/erased_json.rs @@ -1,10 +1,8 @@ use std::sync::Arc; -use axum::{ - http::{header, HeaderValue, StatusCode}, - response::{IntoResponse, Response}, -}; +use axum_core::response::{IntoResponse, Response}; use bytes::{BufMut, Bytes, BytesMut}; +use http::{header, HeaderValue, StatusCode}; use serde_core::Serialize; /// A response type that holds a JSON in serialized form. diff --git a/axum-extra/src/response/file_stream.rs b/axum-extra/src/response/file_stream.rs index b725836a46..ee26148d6e 100644 --- a/axum-extra/src/response/file_stream.rs +++ b/axum-extra/src/response/file_stream.rs @@ -1,4 +1,4 @@ -use axum::{ +use axum_core::{ body, response::{IntoResponse, Response}, BoxError, diff --git a/axum-extra/src/response/mod.rs b/axum-extra/src/response/mod.rs index 40a549f93c..5460c6946f 100644 --- a/axum-extra/src/response/mod.rs +++ b/axum-extra/src/response/mod.rs @@ -60,11 +60,11 @@ macro_rules! mime_response { #[must_use] pub struct $ident(pub T); - impl axum::response::IntoResponse for $ident + impl axum_core::response::IntoResponse for $ident where - T: axum::response::IntoResponse, + T: axum_core::response::IntoResponse, { - fn into_response(self) -> axum::response::Response { + fn into_response(self) -> axum_core::response::Response { ( [( http::header::CONTENT_TYPE, diff --git a/axum-extra/src/response/multiple.rs b/axum-extra/src/response/multiple.rs index a829537867..e17ad968b6 100644 --- a/axum-extra/src/response/multiple.rs +++ b/axum-extra/src/response/multiple.rs @@ -1,6 +1,6 @@ //! Generate forms to use in responses. -use axum::response::{IntoResponse, Response}; +use axum_core::response::{IntoResponse, Response}; use fastrand; use http::{header, HeaderMap, StatusCode}; use mime::Mime; diff --git a/axum-extra/src/typed_header.rs b/axum-extra/src/typed_header.rs index 41cf91819e..70f9ff9ce6 100644 --- a/axum-extra/src/typed_header.rs +++ b/axum-extra/src/typed_header.rs @@ -1,6 +1,6 @@ //! Extractor and response for typed headers. -use axum::{ +use axum_core::{ extract::{FromRequestParts, OptionalFromRequestParts}, response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, }; diff --git a/axum-macros/Cargo.toml b/axum-macros/Cargo.toml index 676f35fd2a..1f433c274a 100644 --- a/axum-macros/Cargo.toml +++ b/axum-macros/Cargo.toml @@ -30,7 +30,7 @@ syn = { version = "2.0", features = [ [dev-dependencies] axum = { path = "../axum", features = ["macros"] } -axum-extra = { path = "../axum-extra", features = ["typed-routing", "cookie-private", "typed-header"] } +axum-extra = { path = "../axum-extra", features = ["typed-routing", "cookie-private", "typed-header", "routing"] } rustversion = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/examples/customize-extractor-error/Cargo.toml b/examples/customize-extractor-error/Cargo.toml index 8e86df21bd..5f40623257 100644 --- a/examples/customize-extractor-error/Cargo.toml +++ b/examples/customize-extractor-error/Cargo.toml @@ -6,7 +6,7 @@ publish = false [dependencies] axum = { path = "../../axum", features = ["macros"] } -axum-extra = { path = "../../axum-extra" } +axum-extra = { path = "../../axum-extra", features = ["with-rejection"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" From 9e6be95ce7e1f6b261219244ec83fcbde16b42d4 Mon Sep 17 00:00:00 2001 From: tottoto Date: Sun, 21 Sep 2025 05:57:04 +0900 Subject: [PATCH 04/10] axum-extra: Remove unused tower dependency (#3486) --- axum-extra/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 807e36a67a..702cd696be 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -65,7 +65,7 @@ with-rejection = ["dep:axum"] # Enabled by docs.rs because it uses all-features # Enables upstream things linked to in docs -__private_docs = ["axum/json", "dep:serde"] +__private_docs = ["axum/json", "dep:serde", "dep:tower"] [dependencies] axum-core = { path = "../axum-core", version = "0.5.2" } @@ -79,7 +79,6 @@ mime = "0.3" pin-project-lite = "0.2" rustversion = "1.0.9" serde_core = "1.0.221" -tower = { version = "0.5.2", default-features = false, features = ["util"] } tower-layer = "0.3" tower-service = "0.3" @@ -104,6 +103,7 @@ typed-json = { version = "0.1.1", optional = true } # doc dependencies serde = { version = "1.0.221", optional = true } +tower = { version = "0.5.2", default-features = false, features = ["util"], optional = true } [dev-dependencies] axum = { path = "../axum", features = ["macros", "__private"] } From 2175c8dcc7b069c5f049cca155a17120f4c2f580 Mon Sep 17 00:00:00 2001 From: tottoto Date: Sun, 21 Sep 2025 06:32:56 +0900 Subject: [PATCH 05/10] axum-extra: Make rustversion and serde_core optional dependency (#3487) --- axum-extra/Cargo.toml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 702cd696be..7479c55230 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -29,16 +29,18 @@ cookie = ["dep:cookie"] cookie-private = ["cookie", "cookie?/private"] cookie-signed = ["cookie", "cookie?/signed"] cookie-key-expansion = ["cookie", "cookie?/key-expansion"] -erased-json = ["dep:serde_json", "dep:typed-json"] +erased-json = ["dep:serde_core", "dep:serde_json", "dep:typed-json"] form = [ "dep:axum", "dep:form_urlencoded", + "dep:serde_core", "dep:serde_html_form", "dep:serde_path_to_error", ] handler = ["dep:axum"] -json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"] +json-deserializer = ["dep:serde_core", "dep:serde_json", "dep:serde_path_to_error"] json-lines = [ + "dep:serde_core", "dep:serde_json", "dep:tokio-util", "dep:tokio-stream", @@ -48,16 +50,22 @@ json-lines = [ ] middleware = ["dep:axum"] multipart = ["dep:multer", "dep:fastrand"] -optional-path = ["dep:axum"] +optional-path = ["dep:axum", "dep:serde_core"] protobuf = ["dep:prost"] -routing = ["axum/original-uri"] +routing = ["axum/original-uri", "dep:rustversion"] scheme = [] -query = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"] +query = [ + "dep:form_urlencoded", + "dep:serde_core", + "dep:serde_html_form", + "dep:serde_path_to_error", +] tracing = ["axum-core/tracing", "axum/tracing", "dep:tracing"] typed-header = ["dep:headers"] typed-routing = [ "dep:axum-macros", "dep:percent-encoding", + "dep:serde_core", "dep:serde_html_form", "dep:form_urlencoded", ] @@ -77,8 +85,6 @@ http-body = "1.0.0" http-body-util = "0.1.0" mime = "0.3" pin-project-lite = "0.2" -rustversion = "1.0.9" -serde_core = "1.0.221" tower-layer = "0.3" tower-service = "0.3" @@ -92,6 +98,8 @@ headers = { version = "0.4.0", optional = true } multer = { version = "3.0.0", optional = true } percent-encoding = { version = "2.1", optional = true } prost = { version = "0.13", optional = true } +rustversion = { version = "1.0.9", optional = true } +serde_core = { version = "1.0.221", 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 } From 82ce9d6168909cf51fc4a4c409b17105ff4108ab Mon Sep 17 00:00:00 2001 From: tottoto Date: Mon, 22 Sep 2025 16:30:03 +0900 Subject: [PATCH 06/10] axum-extra: Add link definition for pull request to changelog (#3492) --- axum-extra/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 6d935ebab5..61539fbe22 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning]. - `WithRejection` extractor requires `with-rejection` feature. [#3298]: https://github.com/tokio-rs/axum/pull/3298 +[#3469]: https://github.com/tokio-rs/axum/pull/3469 [#3485]: https://github.com/tokio-rs/axum/pull/3485 # 0.11.0 From 0c2e4a2d09e36edd7f2d1dd559bfe3214ff39e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Mon, 22 Sep 2025 14:18:46 +0200 Subject: [PATCH 07/10] axum-extra: gate rejection test behind feature (#3493) --- axum-extra/src/routing/typed.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/axum-extra/src/routing/typed.rs b/axum-extra/src/routing/typed.rs index f45ef05381..909a041968 100644 --- a/axum-extra/src/routing/typed.rs +++ b/axum-extra/src/routing/typed.rs @@ -384,10 +384,7 @@ impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, #[cfg(test)] mod tests { - use crate::{ - extract::WithRejection, - routing::{RouterExt, TypedPath}, - }; + use crate::routing::{RouterExt, TypedPath}; use axum::{ extract::rejection::PathRejection, response::{IntoResponse, Response}, @@ -441,9 +438,10 @@ mod tests { assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true&qux=1337"); } + #[cfg(feature = "with-rejection")] #[allow(dead_code)] // just needs to compile fn supports_with_rejection() { - async fn handler(_: WithRejection) {} + async fn handler(_: crate::extract::WithRejection) {} struct MyRejection {} From a77c2cf47834cde2875d2131daffab18ee8b215d Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Sun, 17 Aug 2025 07:14:07 +0200 Subject: [PATCH 08/10] fix(axum-extra): don't require `S` generic param when using `FileStream::from_path()` (#3437) --- axum-extra/src/response/file_stream.rs | 95 +++++++++++++------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/axum-extra/src/response/file_stream.rs b/axum-extra/src/response/file_stream.rs index ee26148d6e..da9d0d7892 100644 --- a/axum-extra/src/response/file_stream.rs +++ b/axum-extra/src/response/file_stream.rs @@ -70,52 +70,6 @@ where } } - /// Create a [`FileStream`] from a file path. - /// - /// # Examples - /// - /// ``` - /// use axum::{ - /// http::StatusCode, - /// response::IntoResponse, - /// Router, - /// routing::get - /// }; - /// use axum_extra::response::file_stream::FileStream; - /// use tokio::fs::File; - /// use tokio_util::io::ReaderStream; - /// - /// async fn file_stream() -> impl IntoResponse { - /// FileStream::>::from_path("test.txt") - /// .await - /// .map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}"))) - /// } - /// - /// let app = Router::new().route("/file-stream", get(file_stream)); - /// # let _: Router = app; - /// ``` - pub async fn from_path(path: impl AsRef) -> io::Result>> { - let file = File::open(&path).await?; - let mut content_size = None; - let mut file_name = None; - - if let Ok(metadata) = file.metadata().await { - content_size = Some(metadata.len()); - } - - if let Some(file_name_os) = path.as_ref().file_name() { - if let Some(file_name_str) = file_name_os.to_str() { - file_name = Some(file_name_str.to_owned()); - } - } - - Ok(FileStream { - stream: ReaderStream::new(file), - file_name, - content_size, - }) - } - /// Set the file name of the [`FileStream`]. /// /// This adds the attachment `Content-Disposition` header with the given `file_name`. @@ -259,6 +213,53 @@ where } } +// Split because the general impl requires to specify `S` and this one does not. +impl FileStream> { + /// Create a [`FileStream`] from a file path. + /// + /// # Examples + /// + /// ``` + /// use axum::{ + /// http::StatusCode, + /// response::IntoResponse, + /// Router, + /// routing::get + /// }; + /// use axum_extra::response::file_stream::FileStream; + /// + /// async fn file_stream() -> impl IntoResponse { + /// FileStream::from_path("test.txt") + /// .await + /// .map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}"))) + /// } + /// + /// let app = Router::new().route("/file-stream", get(file_stream)); + /// # let _: Router = app; + /// ``` + pub async fn from_path(path: impl AsRef) -> io::Result { + let file = File::open(&path).await?; + let mut content_size = None; + let mut file_name = None; + + if let Ok(metadata) = file.metadata().await { + content_size = Some(metadata.len()); + } + + if let Some(file_name_os) = path.as_ref().file_name() { + if let Some(file_name_str) = file_name_os.to_str() { + file_name = Some(file_name_str.to_owned()); + } + } + + Ok(Self { + stream: ReaderStream::new(file), + file_name, + content_size, + }) + } +} + impl IntoResponse for FileStream where S: TryStream + Send + 'static, @@ -474,7 +475,7 @@ mod tests { let app = Router::new().route( "/from_path", get(move || async move { - FileStream::>::from_path(Path::new("CHANGELOG.md")) + FileStream::from_path(Path::new("CHANGELOG.md")) .await .unwrap() .into_response() From e20e90d0b67d0094bed2c55faaa5ae5abf12c094 Mon Sep 17 00:00:00 2001 From: xumaple <45406854+xumaple@users.noreply.github.com> Date: Thu, 9 Oct 2025 01:03:51 -0400 Subject: [PATCH 09/10] Upgrade axum-extra to prost v0.14 (#3517) Co-authored-by: Maple Xu --- Cargo.lock | 8 ++++---- axum-extra/CHANGELOG.md | 2 ++ axum-extra/Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51d5bbd078..01db61dd13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3917,9 +3917,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -3927,9 +3927,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools 0.14.0", diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 61539fbe22..3606160019 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -16,10 +16,12 @@ and this project adheres to [Semantic Versioning]. - `OptionalPath` extractor requires `optional-path` feature. - The routing utilities require `routing` feature. - `WithRejection` extractor requires `with-rejection` feature. +- **breaking:** Upgraded `prost` dependency to v0.14. ([#3517]) [#3298]: https://github.com/tokio-rs/axum/pull/3298 [#3469]: https://github.com/tokio-rs/axum/pull/3469 [#3485]: https://github.com/tokio-rs/axum/pull/3485 +[#3517]: https://github.com/tokio-rs/axum/pull/3517 # 0.11.0 diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 7479c55230..f58def64f7 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -97,7 +97,7 @@ 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.13", optional = true } +prost = { version = "0.14", optional = true } rustversion = { version = "1.0.9", optional = true } serde_core = { version = "1.0.221", optional = true } serde_html_form = { version = "0.2.0", optional = true } From 914a35a012c70d13d4f26cc8564f76525e26afeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Wed, 29 Oct 2025 16:48:41 +0100 Subject: [PATCH 10/10] Release axum-extra 0.12.0 --- Cargo.lock | 2 +- axum-extra/CHANGELOG.md | 2 ++ axum-extra/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01db61dd13..4024335dd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,7 +362,7 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.3" +version = "0.12.0" dependencies = [ "axum", "axum-core", diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 3606160019..676fe217c0 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning]. +# 0.12.0 + - **breaking:** Remove unused `async-stream` feature, which was accidentally introduced as an implicit feature through an optional dependency which was no longer being used ([#3298]) diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index f58def64f7..8e18b8bdb7 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT" name = "axum-extra" readme = "README.md" repository = "https://github.com/tokio-rs/axum" -version = "0.10.3" +version = "0.12.0" [features] default = ["tracing"]