mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-24 00:00:16 +02:00
Remove deprecated extractors from axum-extra (#3599)
This commit is contained in:
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog],
|
||||
and this project adheres to [Semantic Versioning].
|
||||
|
||||
# Unreleased
|
||||
|
||||
- **breaking:** Remove the deprecated `Host`, `Scheme` and `OptionalPath`
|
||||
extractors ([#3599])
|
||||
|
||||
[#3599]: https://github.com/tokio-rs/axum/pull/3599
|
||||
|
||||
# 0.12.5
|
||||
|
||||
- **fixed:** `JsonLines` now correctly respects the default body limit ([#3591])
|
||||
|
||||
@@ -73,10 +73,8 @@ json-lines = [
|
||||
]
|
||||
middleware = ["dep:axum"]
|
||||
multipart = ["dep:multer", "dep:fastrand"]
|
||||
optional-path = ["dep:axum", "dep:serde_core"]
|
||||
protobuf = ["dep:prost"]
|
||||
routing = ["axum/original-uri", "dep:rustversion"]
|
||||
scheme = []
|
||||
query = [
|
||||
"dep:form_urlencoded",
|
||||
"dep:serde_core",
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::rejection::{FailedToResolveHost, HostRejection};
|
||||
use axum_core::{
|
||||
extract::{FromRequestParts, OptionalFromRequestParts},
|
||||
RequestPartsExt,
|
||||
};
|
||||
use http::{
|
||||
header::{HeaderMap, FORWARDED},
|
||||
request::Parts,
|
||||
uri::Authority,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
|
||||
const X_FORWARDED_HOST_HEADER_KEY: &str = "X-Forwarded-Host";
|
||||
|
||||
/// Extractor that resolves the host of the request.
|
||||
///
|
||||
/// Host is resolved through the following, in order:
|
||||
/// - `Forwarded` header
|
||||
/// - `X-Forwarded-Host` header
|
||||
/// - `Host` header
|
||||
/// - Authority of the request URI
|
||||
///
|
||||
/// See <https://www.rfc-editor.org/rfc/rfc9110.html#name-host-and-authority> for the definition of
|
||||
/// host.
|
||||
///
|
||||
/// Note that user agents can set `X-Forwarded-Host` and `Host` headers to arbitrary values so make
|
||||
/// sure to validate them to avoid security issues.
|
||||
#[deprecated = "will be removed in the next version; see https://github.com/tokio-rs/axum/issues/3442"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Host(pub String);
|
||||
|
||||
impl<S> FromRequestParts<S> for Host
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = HostRejection;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.extract::<Option<Self>>()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(HostRejection::FailedToResolveHost(FailedToResolveHost))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> OptionalFromRequestParts<S> for Host
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
_state: &S,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
if let Some(host) = parse_forwarded(&parts.headers) {
|
||||
return Ok(Some(Self(host.to_owned())));
|
||||
}
|
||||
|
||||
if let Some(host) = parts
|
||||
.headers
|
||||
.get(X_FORWARDED_HOST_HEADER_KEY)
|
||||
.and_then(|host| host.to_str().ok())
|
||||
{
|
||||
return Ok(Some(Self(host.to_owned())));
|
||||
}
|
||||
|
||||
if let Some(host) = parts
|
||||
.headers
|
||||
.get(http::header::HOST)
|
||||
.and_then(|host| host.to_str().ok())
|
||||
{
|
||||
return Ok(Some(Self(host.to_owned())));
|
||||
}
|
||||
|
||||
if let Some(authority) = parts.uri.authority() {
|
||||
return Ok(Some(Self(parse_authority(authority).to_owned())));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(warnings)]
|
||||
fn parse_forwarded(headers: &HeaderMap) -> Option<&str> {
|
||||
// if there are multiple `Forwarded` `HeaderMap::get` will return the first one
|
||||
let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?;
|
||||
|
||||
// get the first set of values
|
||||
let first_value = forwarded_values.split(',').nth(0)?;
|
||||
|
||||
// find the value of the `host` field
|
||||
first_value.split(';').find_map(|pair| {
|
||||
let (key, value) = pair.split_once('=')?;
|
||||
key.trim()
|
||||
.eq_ignore_ascii_case("host")
|
||||
.then(|| value.trim().trim_matches('"'))
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_authority(auth: &Authority) -> &str {
|
||||
auth.as_str()
|
||||
.rsplit('@')
|
||||
.next()
|
||||
.expect("split always has at least 1 item")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::TestClient;
|
||||
use axum::{routing::get, Router};
|
||||
use http::{header::HeaderName, Request};
|
||||
|
||||
fn test_client() -> TestClient {
|
||||
async fn host_as_body(Host(host): Host) -> String {
|
||||
host
|
||||
}
|
||||
|
||||
TestClient::new(Router::new().route("/", get(host_as_body)))
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn host_header() {
|
||||
let original_host = "some-domain:123";
|
||||
let host = test_client()
|
||||
.get("/")
|
||||
.header(http::header::HOST, original_host)
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(host, original_host);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn x_forwarded_host_header() {
|
||||
let original_host = "some-domain:456";
|
||||
let host = test_client()
|
||||
.get("/")
|
||||
.header(X_FORWARDED_HOST_HEADER_KEY, original_host)
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(host, original_host);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn x_forwarded_host_precedence_over_host_header() {
|
||||
let x_forwarded_host_header = "some-domain:456";
|
||||
let host_header = "some-domain:123";
|
||||
let host = test_client()
|
||||
.get("/")
|
||||
.header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header)
|
||||
.header(http::header::HOST, host_header)
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(host, x_forwarded_host_header);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn uri_host() {
|
||||
let client = test_client();
|
||||
let port = client.server_port();
|
||||
let host = client.get("/").await.text().await;
|
||||
assert_eq!(host, format!("127.0.0.1:{port}"));
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn ip4_uri_host() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap();
|
||||
let host = parts.extract::<Host>().await.unwrap();
|
||||
assert_eq!(host.0, "127.0.0.1:1234");
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn ip6_uri_host() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
parts.uri = "http://cool:user@[::1]:456/file.txt".parse().unwrap();
|
||||
let host = parts.extract::<Host>().await.unwrap();
|
||||
assert_eq!(host.0, "[::1]:456");
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn missing_host() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
let host = parts.extract::<Host>().await.unwrap_err();
|
||||
assert!(matches!(host, HostRejection::FailedToResolveHost(_)));
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn optional_extractor() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap();
|
||||
let host = parts.extract::<Option<Host>>().await.unwrap();
|
||||
assert!(host.is_some());
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn optional_extractor_none() {
|
||||
let mut parts = Request::new(()).into_parts().0;
|
||||
let host = parts.extract::<Option<Host>>().await.unwrap();
|
||||
assert!(host.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forwarded_parsing() {
|
||||
// the basic case
|
||||
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "192.0.2.60");
|
||||
|
||||
// is case insensitive
|
||||
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "192.0.2.60");
|
||||
|
||||
// ipv6
|
||||
let headers = header_map(&[(FORWARDED, "host=\"[2001:db8:cafe::17]:4711\"")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "[2001:db8:cafe::17]:4711");
|
||||
|
||||
// multiple values in one header
|
||||
let headers = header_map(&[(FORWARDED, "host=192.0.2.60, host=127.0.0.1")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "192.0.2.60");
|
||||
|
||||
// multiple header values
|
||||
let headers = header_map(&[
|
||||
(FORWARDED, "host=192.0.2.60"),
|
||||
(FORWARDED, "host=127.0.0.1"),
|
||||
]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "192.0.2.60");
|
||||
}
|
||||
|
||||
fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
for (key, value) in values {
|
||||
headers.append(key, value.parse().unwrap());
|
||||
}
|
||||
headers
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
//! Additional extractors.
|
||||
|
||||
mod host;
|
||||
pub mod rejection;
|
||||
|
||||
#[cfg(feature = "optional-path")]
|
||||
mod optional_path;
|
||||
|
||||
#[cfg(feature = "cached")]
|
||||
mod cached;
|
||||
|
||||
@@ -27,16 +23,6 @@ mod query;
|
||||
#[cfg(feature = "multipart")]
|
||||
pub mod multipart;
|
||||
|
||||
#[cfg(feature = "scheme")]
|
||||
mod scheme;
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "optional-path")]
|
||||
pub use self::optional_path::OptionalPath;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use self::host::Host;
|
||||
|
||||
#[cfg(feature = "cached")]
|
||||
pub use self::cached::Cached;
|
||||
|
||||
@@ -65,11 +51,6 @@ pub use self::query::{OptionalQueryRejection, Query, QueryRejection};
|
||||
#[cfg(feature = "multipart")]
|
||||
pub use self::multipart::Multipart;
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "scheme")]
|
||||
#[doc(no_inline)]
|
||||
pub use self::scheme::{Scheme, SchemeMissing};
|
||||
|
||||
#[cfg(feature = "json-deserializer")]
|
||||
pub use self::json_deserializer::{
|
||||
JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError,
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
use axum::{
|
||||
extract::{rejection::PathRejection, FromRequestParts, Path},
|
||||
RequestPartsExt,
|
||||
};
|
||||
use serde_core::de::DeserializeOwned;
|
||||
|
||||
/// Extractor that extracts path arguments the same way as [`Path`], except if there aren't any.
|
||||
///
|
||||
/// This extractor can be used in place of `Path` when you have two routes that you want to handle
|
||||
/// in mostly the same way, where one has a path parameter and the other one doesn't.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::NonZeroU32;
|
||||
/// use axum::{
|
||||
/// response::IntoResponse,
|
||||
/// routing::get,
|
||||
/// Router,
|
||||
/// };
|
||||
/// use axum_extra::extract::OptionalPath;
|
||||
///
|
||||
/// async fn render_blog(OptionalPath(page): OptionalPath<NonZeroU32>) -> impl IntoResponse {
|
||||
/// // Convert to u32, default to page 1 if not specified
|
||||
/// let page = page.map_or(1, |param| param.get());
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new()
|
||||
/// .route("/blog", get(render_blog))
|
||||
/// .route("/blog/{page}", get(render_blog));
|
||||
/// # let app: Router = app;
|
||||
/// ```
|
||||
#[deprecated = "Use Option<Path<_>> instead"]
|
||||
#[derive(Debug)]
|
||||
pub struct OptionalPath<T>(pub Option<T>);
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> FromRequestParts<S> for OptionalPath<T>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = PathRejection;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
_: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.extract::<Option<Path<T>>>()
|
||||
.await
|
||||
.map(|opt| Self(opt.map(|Path(x)| x)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(deprecated)]
|
||||
mod tests {
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use axum::{routing::get, Router};
|
||||
|
||||
use super::OptionalPath;
|
||||
use crate::test_helpers::TestClient;
|
||||
|
||||
#[crate::test]
|
||||
async fn supports_128_bit_numbers() {
|
||||
async fn handle(OptionalPath(param): OptionalPath<NonZeroU32>) -> String {
|
||||
let num = param.map_or(0, |p| p.get());
|
||||
format!("Success: {num}")
|
||||
}
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(handle))
|
||||
.route("/{num}", get(handle));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/").await;
|
||||
assert_eq!(res.text().await, "Success: 0");
|
||||
|
||||
let res = client.get("/1").await;
|
||||
assert_eq!(res.text().await, "Success: 1");
|
||||
|
||||
let res = client.get("/0").await;
|
||||
assert_eq!(
|
||||
res.text().await,
|
||||
"Invalid URL: invalid value: integer `0`, expected a nonzero u32"
|
||||
);
|
||||
|
||||
let res = client.get("/NaN").await;
|
||||
assert_eq!(
|
||||
res.text().await,
|
||||
"Invalid URL: Cannot parse `NaN` to a `u32`"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
//! Extractor that parses the scheme of a request.
|
||||
//! See [`Scheme`] for more details.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use axum_core::{__define_rejection as define_rejection, extract::FromRequestParts};
|
||||
use http::{
|
||||
header::{HeaderMap, FORWARDED},
|
||||
request::Parts,
|
||||
};
|
||||
const X_FORWARDED_PROTO_HEADER_KEY: &str = "X-Forwarded-Proto";
|
||||
|
||||
/// Extractor that resolves the scheme / protocol of a request.
|
||||
///
|
||||
/// The scheme is resolved through the following, in order:
|
||||
/// - `Forwarded` header
|
||||
/// - `X-Forwarded-Proto` header
|
||||
/// - Request URI (If the request is an HTTP/2 request! e.g. use `--http2(-prior-knowledge)` with cURL)
|
||||
///
|
||||
/// Note that user agents can set the `X-Forwarded-Proto` header to arbitrary values so make
|
||||
/// sure to validate them to avoid security issues.
|
||||
#[deprecated = "will be removed in the next version; see https://github.com/tokio-rs/axum/issues/3442"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Scheme(pub String);
|
||||
|
||||
define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "No scheme found in request"]
|
||||
/// Rejection type used if the [`Scheme`] extractor is unable to
|
||||
/// resolve a scheme.
|
||||
pub struct SchemeMissing;
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for Scheme
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = SchemeMissing;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
// Within Forwarded header
|
||||
if let Some(scheme) = parse_forwarded(&parts.headers) {
|
||||
return Ok(Self(scheme.to_owned()));
|
||||
}
|
||||
|
||||
// X-Forwarded-Proto
|
||||
if let Some(scheme) = parts
|
||||
.headers
|
||||
.get(X_FORWARDED_PROTO_HEADER_KEY)
|
||||
.and_then(|scheme| scheme.to_str().ok())
|
||||
{
|
||||
return Ok(Self(scheme.to_owned()));
|
||||
}
|
||||
|
||||
// From parts of an HTTP/2 request
|
||||
if let Some(scheme) = parts.uri.scheme_str() {
|
||||
return Ok(Self(scheme.to_owned()));
|
||||
}
|
||||
|
||||
Err(SchemeMissing)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_forwarded(headers: &HeaderMap) -> Option<&str> {
|
||||
// if there are multiple `Forwarded` `HeaderMap::get` will return the first one
|
||||
let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?;
|
||||
|
||||
// get the first set of values
|
||||
let first_value = forwarded_values.split(',').next()?;
|
||||
|
||||
// find the value of the `proto` field
|
||||
first_value.split(';').find_map(|pair| {
|
||||
let (key, value) = pair.split_once('=')?;
|
||||
key.trim()
|
||||
.eq_ignore_ascii_case("proto")
|
||||
.then(|| value.trim().trim_matches('"'))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_helpers::TestClient;
|
||||
use axum::{routing::get, Router};
|
||||
use http::header::HeaderName;
|
||||
|
||||
fn test_client() -> TestClient {
|
||||
async fn scheme_as_body(Scheme(scheme): Scheme) -> String {
|
||||
scheme
|
||||
}
|
||||
|
||||
TestClient::new(Router::new().route("/", get(scheme_as_body)))
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn forwarded_scheme_parsing() {
|
||||
// the basic case
|
||||
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "http");
|
||||
|
||||
// is case insensitive
|
||||
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;PROTO=https;by=203.0.113.43")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "https");
|
||||
|
||||
// multiple values in one header
|
||||
let headers = header_map(&[(FORWARDED, "proto=ftp, proto=https")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "ftp");
|
||||
|
||||
// multiple header values
|
||||
let headers = header_map(&[(FORWARDED, "proto=ftp"), (FORWARDED, "proto=https")]);
|
||||
let value = parse_forwarded(&headers).unwrap();
|
||||
assert_eq!(value, "ftp");
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn x_forwarded_scheme_header() {
|
||||
let original_scheme = "https";
|
||||
let scheme = test_client()
|
||||
.get("/")
|
||||
.header(X_FORWARDED_PROTO_HEADER_KEY, original_scheme)
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(scheme, original_scheme);
|
||||
}
|
||||
|
||||
#[crate::test]
|
||||
async fn precedence_forwarded_over_x_forwarded() {
|
||||
let scheme = test_client()
|
||||
.get("/")
|
||||
.header(X_FORWARDED_PROTO_HEADER_KEY, "https")
|
||||
.header(FORWARDED, "proto=ftp")
|
||||
.await
|
||||
.text()
|
||||
.await;
|
||||
assert_eq!(scheme, "ftp");
|
||||
}
|
||||
|
||||
fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
for (key, value) in values {
|
||||
headers.append(key, value.parse().unwrap());
|
||||
}
|
||||
headers
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@
|
||||
//! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response |
|
||||
//! `middleware` | Enables the [middleware] utilities |
|
||||
//! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor |
|
||||
//! `optional-path` | Enables the [`OptionalPath`](crate::extract::OptionalPath) extractor |
|
||||
//! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response |
|
||||
//! `query` (deprecated) | Enables the [`Query`](crate::extract::Query) extractor |
|
||||
//! `routing` | Enables the [routing] utilities |
|
||||
@@ -88,8 +87,5 @@ pub mod __private {
|
||||
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use axum_macros::__private_axum_test as test;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use axum::test_helpers;
|
||||
|
||||
Reference in New Issue
Block a user