Move TypedHeader to axum-extra (#1850)

Co-authored-by: Michael Scofield <[email protected]>
Co-authored-by: Jonas Platte <[email protected]>
This commit is contained in:
David Pedersen
2023-04-21 17:45:31 +02:00
co-authored by Michael Scofield Jonas Platte
parent 173f9f72b0
commit 877e3fe4de
48 changed files with 227 additions and 246 deletions
+3 -1
View File
@@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **breaking:** Change `sse::Event::json_data` to use `axum_core::Error` as its error type ([#1762])
- **breaking:** Rename `DefaultOnFailedUpdgrade` to `DefaultOnFailedUpgrade` ([#1664])
- **breaking:** Rename `OnFailedUpdgrade` to `OnFailedUpgrade` ([#1664])
- **breaking:** `TypedHeader` has been move to `axum-extra` ([#1850])
- **breaking:** Removed re-exports of `Empty` and `Full`. Use
`axum::body::Body::empty` and `axum::body::Body::from` respectively ([#1789])
- **breaking:** The response returned by `IntoResponse::into_response` must use
@@ -53,8 +54,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1664]: https://github.com/tokio-rs/axum/pull/1664
[#1751]: https://github.com/tokio-rs/axum/pull/1751
[#1762]: https://github.com/tokio-rs/axum/pull/1762
[#1835]: https://github.com/tokio-rs/axum/pull/1835
[#1789]: https://github.com/tokio-rs/axum/pull/1789
[#1835]: https://github.com/tokio-rs/axum/pull/1835
[#1850]: https://github.com/tokio-rs/axum/pull/1850
[#1868]: https://github.com/tokio-rs/axum/pull/1868
# 0.6.16 (18. April, 2023)
-3
View File
@@ -58,7 +58,6 @@ tower-hyper-http-body-compat = { version = "0.1.4", features = ["server", "http1
# optional dependencies
axum-macros = { path = "../axum-macros", version = "0.3.7", optional = true }
base64 = { version = "0.21.0", optional = true }
headers = { version = "0.3.7", optional = true }
multer = { version = "2.0.0", optional = true }
serde_json = { version = "1.0", features = ["raw_value"], optional = true }
serde_path_to_error = { version = "0.1.8", optional = true }
@@ -190,8 +189,6 @@ allowed = [
"futures_core",
"futures_sink",
"futures_util",
"headers",
"headers_core",
"http",
"http_body",
"hyper",
+6 -61
View File
@@ -13,7 +13,6 @@ Types and traits for extracting data from requests.
- [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)
- [Running extractors from middleware](#running-extractors-from-middleware)
- [Wrapping extractors](#wrapping-extractors)
- [Logging rejections](#logging-rejections)
@@ -56,9 +55,8 @@ Some commonly used extractors are:
```rust,no_run
use axum::{
extract::{Request, Json, TypedHeader, Path, Extension, Query},
extract::{Request, Json, Path, Extension, Query},
routing::post,
headers::UserAgent,
http::header::HeaderMap,
body::{Bytes, Body},
Router,
@@ -76,10 +74,6 @@ async fn query(Query(params): Query<HashMap<String, String>>) {}
// `HeaderMap` gives you all the headers
async fn headers(headers: HeaderMap) {}
// `TypedHeader` can be used to extract a single header
// note this requires you've enabled axum's `headers` feature
async fn user_agent(TypedHeader(user_agent): TypedHeader<UserAgent>) {}
// `String` consumes the request body and ensures it is valid utf-8
async fn string(body: String) {}
@@ -102,8 +96,6 @@ struct State { /* ... */ }
let app = Router::new()
.route("/path/:user_id", post(path))
.route("/query", post(query))
.route("/user_agent", post(user_agent))
.route("/headers", post(headers))
.route("/string", post(string))
.route("/bytes", post(bytes))
.route("/json", post(json))
@@ -562,9 +554,8 @@ in your implementation.
```rust
use axum::{
async_trait,
extract::{Extension, FromRequestParts, TypedHeader},
headers::{authorization::Bearer, Authorization},
http::{StatusCode, request::Parts},
extract::{Extension, FromRequestParts},
http::{StatusCode, HeaderMap, request::Parts},
response::{IntoResponse, Response},
routing::get,
Router,
@@ -588,10 +579,9 @@ where
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
// You can either call them directly...
let TypedHeader(Authorization(token)) =
TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
.await
.map_err(|err| err.into_response())?;
let headers = HeaderMap::from_request_parts(parts, state)
.await
.map_err(|err| match err {})?;
// ... or use `extract` / `extract_with_state` from `RequestExt` / `RequestPartsExt`
use axum::RequestPartsExt;
@@ -621,51 +611,6 @@ For security reasons, [`Bytes`] will, by default, not accept bodies larger than
For more details, including how to disable this limit, see [`DefaultBodyLimit`].
# Running extractors from middleware
Extractors can also be run from middleware:
```rust
use axum::{
middleware::{self, Next},
extract::{TypedHeader, Request, FromRequestParts},
http::StatusCode,
response::Response,
headers::authorization::{Authorization, Bearer},
RequestPartsExt, Router,
};
async fn auth_middleware(
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
// running extractors requires a `axum::http::request::Parts`
let (mut parts, body) = request.into_parts();
// `TypedHeader<Authorization<Bearer>>` extracts the auth token
let auth: TypedHeader<Authorization<Bearer>> = parts.extract()
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
if !token_is_valid(auth.token()) {
return Err(StatusCode::UNAUTHORIZED);
}
// reconstruct the request
let request = Request::from_parts(parts, body);
Ok(next.run(request).await)
}
fn token_is_valid(token: &str) -> bool {
// ...
# false
}
let app = Router::new().layer(middleware::from_fn(auth_middleware));
# let _: Router = app;
```
# Wrapping extractors
If you want write an extractor that generically wraps another extractor (that
+5 -11
View File
@@ -18,9 +18,7 @@ let app = Router::new()
async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {}", uri))
}
# async {
# hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
# };
# let _: Router = app;
```
Fallbacks only apply to routes that aren't matched by anything in the
@@ -40,10 +38,8 @@ async fn handler() {}
let app = Router::new().fallback(handler);
# async {
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
# };
```
@@ -55,9 +51,7 @@ use axum::handler::HandlerWithoutStateExt;
async fn handler() {}
# async {
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(handler.into_make_service())
.await
.unwrap();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, handler.into_make_service()).await.unwrap();
# };
```
-4
View File
@@ -76,10 +76,6 @@ pub use self::request_parts::OriginalUri;
#[doc(inline)]
pub use self::ws::WebSocketUpgrade;
#[cfg(feature = "headers")]
#[doc(no_inline)]
pub use crate::TypedHeader;
// this is duplicated in `axum-extra/src/extract/form.rs`
pub(super) fn has_content_type(headers: &HeaderMap, expected_content_type: &mime::Mime) -> bool {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
-3
View File
@@ -207,6 +207,3 @@ composite_rejection! {
MatchedPathMissing,
}
}
#[cfg(feature = "headers")]
pub use crate::typed_header::{TypedHeaderRejection, TypedHeaderRejectionReason};
-11
View File
@@ -336,7 +336,6 @@
//!
//! Name | Description | Default?
//! ---|---|---
//! `headers` | Enables extracting typed headers via [`TypedHeader`] | No
//! `http1` | Enables hyper's `http1` feature | Yes
//! `http2` | Enables hyper's `http2` feature | No
//! `json` | Enables the [`Json`] type and some similar convenience functionality | Yes
@@ -351,7 +350,6 @@
//! `form` | Enables the `Form` extractor | Yes
//! `query` | Enables the `Query` extractor | Yes
//!
//! [`TypedHeader`]: crate::extract::TypedHeader
//! [`MatchedPath`]: crate::extract::MatchedPath
//! [`Multipart`]: crate::extract::Multipart
//! [`OriginalUri`]: crate::extract::OriginalUri
@@ -435,8 +433,6 @@ mod form;
#[cfg(feature = "json")]
mod json;
mod service_ext;
#[cfg(feature = "headers")]
mod typed_header;
mod util;
pub mod body;
@@ -454,9 +450,6 @@ mod test_helpers;
#[doc(no_inline)]
pub use async_trait::async_trait;
#[cfg(feature = "headers")]
#[doc(no_inline)]
pub use headers;
#[doc(no_inline)]
pub use http;
@@ -468,10 +461,6 @@ pub use self::json::Json;
#[doc(inline)]
pub use self::routing::Router;
#[doc(inline)]
#[cfg(feature = "headers")]
pub use self::typed_header::TypedHeader;
#[doc(inline)]
#[cfg(feature = "form")]
pub use self::form::Form;
+17 -10
View File
@@ -61,31 +61,38 @@ use tower_service::Service;
/// ```rust
/// use axum::{
/// Router,
/// extract::{Request, TypedHeader},
/// http::StatusCode,
/// headers::authorization::{Authorization, Bearer},
/// extract::Request,
/// http::{StatusCode, HeaderMap},
/// middleware::{self, Next},
/// response::Response,
/// routing::get,
/// };
///
/// async fn auth(
/// // run the `TypedHeader` extractor
/// TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
/// // run the `HeaderMap` extractor
/// headers: HeaderMap,
/// // you can also add more extractors here but the last
/// // extractor must implement `FromRequest` which
/// // `Request` does
/// request: Request,
/// next: Next,
/// ) -> Result<Response, StatusCode> {
/// if token_is_valid(auth.token()) {
/// let response = next.run(request).await;
/// Ok(response)
/// } else {
/// Err(StatusCode::UNAUTHORIZED)
/// match get_token(&headers) {
/// Some(token) if token_is_valid(token) => {
/// let response = next.run(request).await;
/// Ok(response)
/// }
/// _ => {
/// Err(StatusCode::UNAUTHORIZED)
/// }
/// }
/// }
///
/// fn get_token(headers: &HeaderMap) -> Option<&str> {
/// // ...
/// # None
/// }
///
/// fn token_is_valid(token: &str) -> bool {
/// // ...
/// # false
-4
View File
@@ -12,10 +12,6 @@ pub mod sse;
#[cfg(feature = "json")]
pub use crate::Json;
#[doc(no_inline)]
#[cfg(feature = "headers")]
pub use crate::TypedHeader;
#[cfg(feature = "form")]
#[doc(no_inline)]
pub use crate::form::Form;
+4 -4
View File
@@ -627,10 +627,10 @@ where
fn layer<L>(self, layer: L) -> Endpoint<S>
where
L: Layer<Route> + Clone + Send + 'static,
L::Service: Service<Request> + Clone + Send + 'static,
<L::Service as Service<Request>>::Response: IntoResponse + 'static,
<L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request>>::Future: Send + 'static,
L::Service: Service<Request<Body>> + Clone + Send + 'static,
<L::Service as Service<Request<Body>>>::Response: IntoResponse + 'static,
<L::Service as Service<Request<Body>>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request<Body>>>::Future: Send + 'static,
{
match self {
Endpoint::MethodRouter(method_router) => {
-204
View File
@@ -1,204 +0,0 @@
use crate::extract::FromRequestParts;
use async_trait::async_trait;
use axum_core::response::{IntoResponse, IntoResponseParts, Response, ResponseParts};
use headers::HeaderMapExt;
use http::request::Parts;
use std::convert::Infallible;
/// Extractor and response that works with typed header values from [`headers`].
///
/// # As extractor
///
/// In general, it's recommended to extract only the needed headers via `TypedHeader` rather than
/// removing all headers with the `HeaderMap` extractor.
///
/// ```rust,no_run
/// use axum::{
/// TypedHeader,
/// headers::UserAgent,
/// routing::get,
/// Router,
/// };
///
/// async fn users_teams_show(
/// TypedHeader(user_agent): TypedHeader<UserAgent>,
/// ) {
/// // ...
/// }
///
/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
/// # let _: Router = app;
/// ```
///
/// # As response
///
/// ```rust
/// use axum::{
/// TypedHeader,
/// response::IntoResponse,
/// headers::ContentType,
/// };
///
/// async fn handler() -> (TypedHeader<ContentType>, &'static str) {
/// (
/// TypedHeader(ContentType::text_utf8()),
/// "Hello, World!",
/// )
/// }
/// ```
#[cfg(feature = "headers")]
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct TypedHeader<T>(pub T);
#[async_trait]
impl<T, S> FromRequestParts<S> for TypedHeader<T>
where
T: headers::Header,
S: Send + Sync,
{
type Rejection = TypedHeaderRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let mut values = parts.headers.get_all(T::name()).iter();
let is_missing = values.size_hint() == (0, Some(0));
T::decode(&mut values)
.map(Self)
.map_err(|err| TypedHeaderRejection {
name: T::name(),
reason: if is_missing {
// Report a more precise rejection for the missing header case.
TypedHeaderRejectionReason::Missing
} else {
TypedHeaderRejectionReason::Error(err)
},
})
}
}
axum_core::__impl_deref!(TypedHeader);
impl<T> IntoResponseParts for TypedHeader<T>
where
T: headers::Header,
{
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().typed_insert(self.0);
Ok(res)
}
}
impl<T> IntoResponse for TypedHeader<T>
where
T: headers::Header,
{
fn into_response(self) -> Response {
let mut res = ().into_response();
res.headers_mut().typed_insert(self.0);
res
}
}
/// Rejection used for [`TypedHeader`](super::TypedHeader).
#[cfg(feature = "headers")]
#[derive(Debug)]
pub struct TypedHeaderRejection {
name: &'static http::header::HeaderName,
reason: TypedHeaderRejectionReason,
}
impl TypedHeaderRejection {
/// Name of the header that caused the rejection
pub fn name(&self) -> &http::header::HeaderName {
self.name
}
/// Reason why the header extraction has failed
pub fn reason(&self) -> &TypedHeaderRejectionReason {
&self.reason
}
}
/// Additional information regarding a [`TypedHeaderRejection`]
#[cfg(feature = "headers")]
#[derive(Debug)]
#[non_exhaustive]
pub enum TypedHeaderRejectionReason {
/// The header was missing from the HTTP request
Missing,
/// An error occured when parsing the header from the HTTP request
Error(headers::Error),
}
impl IntoResponse for TypedHeaderRejection {
fn into_response(self) -> Response {
(http::StatusCode::BAD_REQUEST, self.to_string()).into_response()
}
}
impl std::fmt::Display for TypedHeaderRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.reason {
TypedHeaderRejectionReason::Missing => {
write!(f, "Header of type `{}` was missing", self.name)
}
TypedHeaderRejectionReason::Error(err) => {
write!(f, "{} ({})", err, self.name)
}
}
}
}
impl std::error::Error for TypedHeaderRejection {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.reason {
TypedHeaderRejectionReason::Error(err) => Some(err),
TypedHeaderRejectionReason::Missing => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{response::IntoResponse, routing::get, test_helpers::*, Router};
#[crate::test]
async fn typed_header() {
async fn handle(
TypedHeader(user_agent): TypedHeader<headers::UserAgent>,
TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> impl IntoResponse {
let user_agent = user_agent.as_str();
let cookies = cookies.iter().collect::<Vec<_>>();
format!("User-Agent={user_agent:?}, Cookie={cookies:?}")
}
let app = Router::new().route("/", get(handle));
let client = TestClient::new(app);
let res = client
.get("/")
.header("user-agent", "foobar")
.header("cookie", "a=1; b=2")
.header("cookie", "c=3")
.send()
.await;
let body = res.text().await;
assert_eq!(
body,
r#"User-Agent="foobar", Cookie=[("a", "1"), ("b", "2"), ("c", "3")]"#
);
let res = client.get("/").header("user-agent", "foobar").send().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 body = res.text().await;
assert_eq!(body, "Header of type `user-agent` was missing");
}
}