Skip to content

Commit 5bb8df1

Browse files
authored
Add trait IntoResponseHeaders (#649)
* Introduce IntoResponseHeaders trait * Implement IntoResponseHeaders for HeaderMap * Add impl IntoResponse for impl IntoResponseHeaders … and update IntoResponse impls that use HeaderMap to be generic instead. * Add impl IntoResponseHeaders for Headers … and remove IntoResponse impls that use it. * axum-debug: Fix grammar in docs * Explain confusing error message in docs
1 parent 9f5dc45 commit 5bb8df1

File tree

4 files changed

+186
-93
lines changed

4 files changed

+186
-93
lines changed

axum-core/src/response/headers.rs

Lines changed: 27 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
use super::{IntoResponse, Response};
2-
use crate::body::boxed;
3-
use bytes::Bytes;
1+
use super::{IntoResponse, IntoResponseHeaders, Response};
42
use http::{
5-
header::{HeaderMap, HeaderName, HeaderValue},
3+
header::{HeaderName, HeaderValue},
64
StatusCode,
75
};
8-
use http_body::{Empty, Full};
96
use std::{convert::TryInto, fmt};
107

118
/// A response with headers.
@@ -54,102 +51,53 @@ use std::{convert::TryInto, fmt};
5451
#[derive(Clone, Copy, Debug)]
5552
pub struct Headers<H>(pub H);
5653

57-
impl<H> Headers<H> {
58-
fn try_into_header_map<K, V>(self) -> Result<HeaderMap, Response>
59-
where
60-
H: IntoIterator<Item = (K, V)>,
61-
K: TryInto<HeaderName>,
62-
K::Error: fmt::Display,
63-
V: TryInto<HeaderValue>,
64-
V::Error: fmt::Display,
65-
{
66-
self.0
67-
.into_iter()
68-
.map(|(key, value)| {
69-
let key = key.try_into().map_err(Either::A)?;
70-
let value = value.try_into().map_err(Either::B)?;
71-
Ok((key, value))
72-
})
73-
.collect::<Result<_, _>>()
74-
.map_err(|err| {
75-
let err = match err {
76-
Either::A(err) => err.to_string(),
77-
Either::B(err) => err.to_string(),
78-
};
79-
80-
let body = boxed(Full::new(Bytes::copy_from_slice(err.as_bytes())));
81-
let mut res = Response::new(body);
82-
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
83-
res
84-
})
85-
}
86-
}
87-
88-
impl<H, K, V> IntoResponse for Headers<H>
54+
impl<H, K, V> IntoResponseHeaders for Headers<H>
8955
where
9056
H: IntoIterator<Item = (K, V)>,
9157
K: TryInto<HeaderName>,
9258
K::Error: fmt::Display,
9359
V: TryInto<HeaderValue>,
9460
V::Error: fmt::Display,
9561
{
96-
fn into_response(self) -> Response {
97-
let headers = self.try_into_header_map();
98-
99-
match headers {
100-
Ok(headers) => {
101-
let mut res = Response::new(boxed(Empty::new()));
102-
*res.headers_mut() = headers;
103-
res
104-
}
105-
Err(err) => err,
62+
type IntoIter = IntoIter<H::IntoIter>;
63+
64+
fn into_headers(self) -> Self::IntoIter {
65+
IntoIter {
66+
inner: self.0.into_iter(),
10667
}
10768
}
10869
}
10970

110-
impl<H, T, K, V> IntoResponse for (Headers<H>, T)
111-
where
112-
T: IntoResponse,
113-
H: IntoIterator<Item = (K, V)>,
114-
K: TryInto<HeaderName>,
115-
K::Error: fmt::Display,
116-
V: TryInto<HeaderValue>,
117-
V::Error: fmt::Display,
118-
{
119-
fn into_response(self) -> Response {
120-
let headers = match self.0.try_into_header_map() {
121-
Ok(headers) => headers,
122-
Err(res) => return res,
123-
};
124-
125-
(headers, self.1).into_response()
126-
}
71+
#[doc(hidden)]
72+
#[derive(Debug)]
73+
pub struct IntoIter<H> {
74+
inner: H,
12775
}
12876

129-
impl<H, T, K, V> IntoResponse for (StatusCode, Headers<H>, T)
77+
impl<H, K, V> Iterator for IntoIter<H>
13078
where
131-
T: IntoResponse,
132-
H: IntoIterator<Item = (K, V)>,
79+
H: Iterator<Item = (K, V)>,
13380
K: TryInto<HeaderName>,
13481
K::Error: fmt::Display,
13582
V: TryInto<HeaderValue>,
13683
V::Error: fmt::Display,
13784
{
138-
fn into_response(self) -> Response {
139-
let headers = match self.1.try_into_header_map() {
140-
Ok(headers) => headers,
141-
Err(res) => return res,
142-
};
143-
144-
(self.0, headers, self.2).into_response()
85+
type Item = Result<(Option<HeaderName>, HeaderValue), Response>;
86+
87+
fn next(&mut self) -> Option<Self::Item> {
88+
self.inner.next().map(|(key, value)| {
89+
let key = key
90+
.try_into()
91+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response())?;
92+
let value = value
93+
.try_into()
94+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response())?;
95+
96+
Ok((Some(key), value))
97+
})
14598
}
14699
}
147100

148-
enum Either<A, B> {
149-
A(A),
150-
B(B),
151-
}
152-
153101
#[cfg(test)]
154102
mod tests {
155103
use super::*;

axum-core/src/response/mod.rs

Lines changed: 127 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ use crate::{
1010
};
1111
use bytes::Bytes;
1212
use http::{
13-
header::{self, HeaderMap, HeaderValue},
13+
header::{self, HeaderMap, HeaderName, HeaderValue},
1414
StatusCode,
1515
};
1616
use http_body::{
1717
combinators::{MapData, MapErr},
1818
Empty, Full,
1919
};
20-
use std::{borrow::Cow, convert::Infallible};
20+
use std::{borrow::Cow, convert::Infallible, iter};
2121

2222
mod headers;
2323

@@ -148,6 +148,42 @@ pub trait IntoResponse {
148148
fn into_response(self) -> Response;
149149
}
150150

151+
/// Trait for generating response headers.
152+
///
153+
154+
/// **Note: If you see this trait not being implemented in an error message, you are almost
155+
/// certainly being mislead by the compiler¹. Look for the following snippet in the output and
156+
/// check [`IntoResponse`]'s documentation if you find it:**
157+
///
158+
/// ```text
159+
/// note: required because of the requirements on the impl of `IntoResponse` for `<type>`
160+
/// ```
161+
///
162+
/// Any type that implements this trait automatically implements `IntoResponse` as well, but can
163+
/// also be used in a tuple like `(StatusCode, Self)`, `(Self, impl IntoResponseHeaders)`,
164+
/// `(StatusCode, Self, impl IntoResponseHeaders, impl IntoResponse)` and so on.
165+
///
166+
/// This trait can't currently be implemented outside of axum.
167+
///
168+
/// ¹ See also [this rustc issue](https://github.com/rust-lang/rust/issues/22590)
169+
pub trait IntoResponseHeaders {
170+
/// The return type of [`.into_headers()`].
171+
///
172+
/// The iterator item is a `Result` to allow the implementation to return a server error
173+
/// instead.
174+
///
175+
/// The header name is optional because `HeaderMap`s iterator doesn't yield it multiple times
176+
/// for headers that have multiple values, to avoid unnecessary copies.
177+
#[doc(hidden)]
178+
type IntoIter: IntoIterator<Item = Result<(Option<HeaderName>, HeaderValue), Response>>;
179+
180+
/// Attempt to turn `self` into a list of headers.
181+
///
182+
/// In practice, only the implementation for `axum::response::Headers` ever returns `Err(_)`.
183+
#[doc(hidden)]
184+
fn into_headers(self) -> Self::IntoIter;
185+
}
186+
151187
impl IntoResponse for () {
152188
fn into_response(self) -> Response {
153189
Response::new(boxed(Empty::new()))
@@ -320,6 +356,21 @@ impl IntoResponse for StatusCode {
320356
}
321357
}
322358

359+
impl<H> IntoResponse for H
360+
where
361+
H: IntoResponseHeaders,
362+
{
363+
fn into_response(self) -> Response {
364+
let mut res = Response::new(boxed(Empty::new()));
365+
366+
if let Err(e) = try_extend_headers(res.headers_mut(), self.into_headers()) {
367+
return e;
368+
}
369+
370+
res
371+
}
372+
}
373+
323374
impl<T> IntoResponse for (StatusCode, T)
324375
where
325376
T: IntoResponse,
@@ -331,33 +382,98 @@ where
331382
}
332383
}
333384

334-
impl<T> IntoResponse for (HeaderMap, T)
385+
impl<H, T> IntoResponse for (H, T)
335386
where
387+
H: IntoResponseHeaders,
336388
T: IntoResponse,
337389
{
338390
fn into_response(self) -> Response {
339391
let mut res = self.1.into_response();
340-
res.headers_mut().extend(self.0);
392+
393+
if let Err(e) = try_extend_headers(res.headers_mut(), self.0.into_headers()) {
394+
return e;
395+
}
396+
341397
res
342398
}
343399
}
344400

345-
impl<T> IntoResponse for (StatusCode, HeaderMap, T)
401+
impl<H, T> IntoResponse for (StatusCode, H, T)
346402
where
403+
H: IntoResponseHeaders,
347404
T: IntoResponse,
348405
{
349406
fn into_response(self) -> Response {
350407
let mut res = self.2.into_response();
351408
*res.status_mut() = self.0;
352-
res.headers_mut().extend(self.1);
409+
410+
if let Err(e) = try_extend_headers(res.headers_mut(), self.1.into_headers()) {
411+
return e;
412+
}
413+
353414
res
354415
}
355416
}
356417

357-
impl IntoResponse for HeaderMap {
358-
fn into_response(self) -> Response {
359-
let mut res = Response::new(boxed(Empty::new()));
360-
*res.headers_mut() = self;
361-
res
418+
impl IntoResponseHeaders for HeaderMap {
419+
// FIXME: Use type_alias_impl_trait when available
420+
type IntoIter = iter::Map<
421+
http::header::IntoIter<HeaderValue>,
422+
fn(
423+
(Option<HeaderName>, HeaderValue),
424+
) -> Result<(Option<HeaderName>, HeaderValue), Response>,
425+
>;
426+
427+
fn into_headers(self) -> Self::IntoIter {
428+
self.into_iter().map(Ok)
429+
}
430+
}
431+
432+
// Slightly adjusted version of `impl<T> Extend<(Option<HeaderName>, T)> for HeaderMap<T>`.
433+
// Accepts an iterator that returns Results and short-circuits on an `Err`.
434+
fn try_extend_headers(
435+
headers: &mut HeaderMap,
436+
iter: impl IntoIterator<Item = Result<(Option<HeaderName>, HeaderValue), Response>>,
437+
) -> Result<(), Response> {
438+
use http::header::Entry;
439+
440+
let mut iter = iter.into_iter();
441+
442+
// The structure of this is a bit weird, but it is mostly to make the
443+
// borrow checker happy.
444+
let (mut key, mut val) = match iter.next().transpose()? {
445+
Some((Some(key), val)) => (key, val),
446+
Some((None, _)) => panic!("expected a header name, but got None"),
447+
None => return Ok(()),
448+
};
449+
450+
'outer: loop {
451+
let mut entry = match headers.entry(key) {
452+
Entry::Occupied(mut e) => {
453+
// Replace all previous values while maintaining a handle to
454+
// the entry.
455+
e.insert(val);
456+
e
457+
}
458+
Entry::Vacant(e) => e.insert_entry(val),
459+
};
460+
461+
// As long as `HeaderName` is none, keep inserting the value into
462+
// the current entry
463+
loop {
464+
match iter.next().transpose()? {
465+
Some((Some(k), v)) => {
466+
key = k;
467+
val = v;
468+
continue 'outer;
469+
}
470+
Some((None, v)) => {
471+
entry.append(v);
472+
}
473+
None => {
474+
return Ok(());
475+
}
476+
}
477+
}
362478
}
363479
}

axum-debug/src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
//! ```
2222
//!
2323
//! You will get a long error message about function not implementing [`Handler`] trait. But why
24-
//! this function does not implement it? To figure it out [`debug_handler`] macro can be used.
24+
//! does this function not implement it? To figure it out, the [`debug_handler`] macro can be used.
2525
//!
2626
//! ```rust,compile_fail
2727
//! # use axum::{routing::get, Router};
@@ -89,6 +89,34 @@
8989
//! async fn handler(request: Request<BoxBody>) {}
9090
//! ```
9191
//!
92+
//! # Known limitations
93+
//!
94+
//! If your response type doesn't implement `IntoResponse`, you will get a slightly confusing error
95+
//! message:
96+
//!
97+
//! ```text
98+
//! error[E0277]: the trait bound `bool: axum_core::response::IntoResponseHeaders` is not satisfied
99+
//! --> tests/fail/wrong_return_type.rs:4:23
100+
//! |
101+
//! 4 | async fn handler() -> bool {
102+
//! | ^^^^ the trait `axum_core::response::IntoResponseHeaders` is not implemented for `bool`
103+
//! |
104+
//! = note: required because of the requirements on the impl of `IntoResponse` for `bool`
105+
//! note: required by a bound in `__axum_debug_check_handler_into_response::{closure#0}::check`
106+
//! --> tests/fail/wrong_return_type.rs:4:23
107+
//! |
108+
//! 4 | async fn handler() -> bool {
109+
//! | ^^^^ required by this bound in `__axum_debug_check_handler_into_response::{closure#0}::check`
110+
//! ```
111+
//!
112+
//! The main error message when `IntoResponse` isn't implemented will also ways be for a different
113+
//! trait, `IntoResponseHeaders`, not being implemented. This trait is not meant to be implemented
114+
//! for types outside of axum and what you really need to do is change your return type or implement
115+
//! `IntoResponse` for it (if it is your own type that you want to return directly from handlers).
116+
//!
117+
//! This issue is not specific to axum and cannot be fixed by us. For more details, see the
118+
//! [rustc issue about it](https://github.com/rust-lang/rust/issues/22590).
119+
//!
92120
//! # Performance
93121
//!
94122
//! Macros in this crate have no effect when using release profile. (eg. `cargo build --release`)

axum-debug/tests/fail/wrong_return_type.stderr

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
1+
error[E0277]: the trait bound `bool: axum_core::response::IntoResponseHeaders` is not satisfied
22
--> tests/fail/wrong_return_type.rs:4:23
33
|
44
4 | async fn handler() -> bool {
5-
| ^^^^ the trait `IntoResponse` is not implemented for `bool`
5+
| ^^^^ the trait `axum_core::response::IntoResponseHeaders` is not implemented for `bool`
66
|
7+
= note: required because of the requirements on the impl of `IntoResponse` for `bool`
78
note: required by a bound in `__axum_debug_check_handler_into_response::{closure#0}::check`
89
--> tests/fail/wrong_return_type.rs:4:23
910
|

0 commit comments

Comments
 (0)