Skip to content

Commit 35a6601

Browse files
authored
refactor: report custom reason phrase in error message (#2697)
Closes #2565
1 parent 5e03d04 commit 35a6601

File tree

5 files changed

+123
-38
lines changed

5 files changed

+123
-38
lines changed

src/async_impl/response.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,9 @@ impl Response {
385385
/// ```
386386
pub fn error_for_status(self) -> crate::Result<Self> {
387387
let status = self.status();
388+
let reason = self.extensions().get::<hyper::ext::ReasonPhrase>().cloned();
388389
if status.is_client_error() || status.is_server_error() {
389-
Err(crate::error::status_code(*self.url, status))
390+
Err(crate::error::status_code(*self.url, status, reason))
390391
} else {
391392
Ok(self)
392393
}
@@ -415,8 +416,9 @@ impl Response {
415416
/// ```
416417
pub fn error_for_status_ref(&self) -> crate::Result<&Self> {
417418
let status = self.status();
419+
let reason = self.extensions().get::<hyper::ext::ReasonPhrase>().cloned();
418420
if status.is_client_error() || status.is_server_error() {
419-
Err(crate::error::status_code(*self.url.clone(), status))
421+
Err(crate::error::status_code(*self.url.clone(), status, reason))
420422
} else {
421423
Ok(self)
422424
}

src/connect.rs

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,7 @@ mod socks {
12891289
}
12901290

12911291
mod verbose {
1292+
use crate::util::Escape;
12921293
use hyper::rt::{Read, ReadBufCursor, Write};
12931294
use hyper_util::client::legacy::connect::{Connected, Connection};
12941295
use std::cmp::min;
@@ -1339,7 +1340,7 @@ mod verbose {
13391340
let mut vbuf = hyper::rt::ReadBuf::uninit(unsafe { buf.as_mut() });
13401341
match Pin::new(&mut self.inner).poll_read(cx, vbuf.unfilled()) {
13411342
Poll::Ready(Ok(())) => {
1342-
log::trace!("{:08x} read: {:?}", self.id, Escape(vbuf.filled()));
1343+
log::trace!("{:08x} read: {:?}", self.id, Escape::new(vbuf.filled()));
13431344
let len = vbuf.filled().len();
13441345
// SAFETY: The two cursors were for the same buffer. What was
13451346
// filled in one is safe in the other.
@@ -1362,7 +1363,7 @@ mod verbose {
13621363
) -> Poll<Result<usize, std::io::Error>> {
13631364
match Pin::new(&mut self.inner).poll_write(cx, buf) {
13641365
Poll::Ready(Ok(n)) => {
1365-
log::trace!("{:08x} write: {:?}", self.id, Escape(&buf[..n]));
1366+
log::trace!("{:08x} write: {:?}", self.id, Escape::new(&buf[..n]));
13661367
Poll::Ready(Ok(n))
13671368
}
13681369
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
@@ -1415,35 +1416,6 @@ mod verbose {
14151416
}
14161417
}
14171418

1418-
struct Escape<'a>(&'a [u8]);
1419-
1420-
impl fmt::Debug for Escape<'_> {
1421-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1422-
write!(f, "b\"")?;
1423-
for &c in self.0 {
1424-
// https://doc.rust-lang.org/reference.html#byte-escapes
1425-
if c == b'\n' {
1426-
write!(f, "\\n")?;
1427-
} else if c == b'\r' {
1428-
write!(f, "\\r")?;
1429-
} else if c == b'\t' {
1430-
write!(f, "\\t")?;
1431-
} else if c == b'\\' || c == b'"' {
1432-
write!(f, "\\{}", c as char)?;
1433-
} else if c == b'\0' {
1434-
write!(f, "\\0")?;
1435-
// ASCII printable
1436-
} else if c >= 0x20 && c < 0x7f {
1437-
write!(f, "{}", c as char)?;
1438-
} else {
1439-
write!(f, "\\x{c:02x}")?;
1440-
}
1441-
}
1442-
write!(f, "\"")?;
1443-
Ok(())
1444-
}
1445-
}
1446-
14471419
struct Vectored<'a, 'b> {
14481420
bufs: &'a [IoSlice<'b>],
14491421
nwritten: usize,
@@ -1457,7 +1429,7 @@ mod verbose {
14571429
break;
14581430
}
14591431
let n = min(left, buf.len());
1460-
Escape(&buf[..n]).fmt(f)?;
1432+
Escape::new(&buf[..n]).fmt(f)?;
14611433
left -= n;
14621434
}
14631435
Ok(())

src/error.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::error::Error as StdError;
33
use std::fmt;
44
use std::io;
55

6+
use crate::util::Escape;
67
use crate::{StatusCode, Url};
78

89
/// A `Result` alias where the `Err` case is `reqwest::Error`.
@@ -93,8 +94,9 @@ impl Error {
9394
}
9495

9596
/// Returns true if the error is from `Response::error_for_status`.
97+
#[cfg(not(target_arch = "wasm32"))]
9698
pub fn is_status(&self) -> bool {
97-
matches!(self.inner.kind, Kind::Status(_))
99+
matches!(self.inner.kind, Kind::Status(_, _))
98100
}
99101

100102
/// Returns true if the error is related to a timeout.
@@ -152,7 +154,10 @@ impl Error {
152154
/// Returns the status code, if the error was generated from a response.
153155
pub fn status(&self) -> Option<StatusCode> {
154156
match self.inner.kind {
157+
#[cfg(target_arch = "wasm32")]
155158
Kind::Status(code) => Some(code),
159+
#[cfg(not(target_arch = "wasm32"))]
160+
Kind::Status(code, _) => Some(code),
156161
_ => None,
157162
}
158163
}
@@ -204,6 +209,7 @@ impl fmt::Display for Error {
204209
Kind::Decode => f.write_str("error decoding response body")?,
205210
Kind::Redirect => f.write_str("error following redirect")?,
206211
Kind::Upgrade => f.write_str("error upgrading connection")?,
212+
#[cfg(target_arch = "wasm32")]
207213
Kind::Status(ref code) => {
208214
let prefix = if code.is_client_error() {
209215
"HTTP status client error"
@@ -213,6 +219,25 @@ impl fmt::Display for Error {
213219
};
214220
write!(f, "{prefix} ({code})")?;
215221
}
222+
#[cfg(not(target_arch = "wasm32"))]
223+
Kind::Status(ref code, ref reason) => {
224+
let prefix = if code.is_client_error() {
225+
"HTTP status client error"
226+
} else {
227+
debug_assert!(code.is_server_error());
228+
"HTTP status server error"
229+
};
230+
if let Some(reason) = reason {
231+
write!(
232+
f,
233+
"{prefix} ({} {})",
234+
code.as_str(),
235+
Escape::new(reason.as_bytes())
236+
)?;
237+
} else {
238+
write!(f, "{prefix} ({code})")?;
239+
}
240+
}
216241
};
217242

218243
if let Some(url) = &self.inner.url {
@@ -248,6 +273,9 @@ pub(crate) enum Kind {
248273
Builder,
249274
Request,
250275
Redirect,
276+
#[cfg(not(target_arch = "wasm32"))]
277+
Status(StatusCode, Option<hyper::ext::ReasonPhrase>),
278+
#[cfg(target_arch = "wasm32")]
251279
Status(StatusCode),
252280
Body,
253281
Decode,
@@ -276,8 +304,20 @@ pub(crate) fn redirect<E: Into<BoxError>>(e: E, url: Url) -> Error {
276304
Error::new(Kind::Redirect, Some(e)).with_url(url)
277305
}
278306

279-
pub(crate) fn status_code(url: Url, status: StatusCode) -> Error {
280-
Error::new(Kind::Status(status), None::<Error>).with_url(url)
307+
pub(crate) fn status_code(
308+
url: Url,
309+
status: StatusCode,
310+
#[cfg(not(target_arch = "wasm32"))] reason: Option<hyper::ext::ReasonPhrase>,
311+
) -> Error {
312+
Error::new(
313+
Kind::Status(
314+
status,
315+
#[cfg(not(target_arch = "wasm32"))]
316+
reason,
317+
),
318+
None::<Error>,
319+
)
320+
.with_url(url)
281321
}
282322

283323
pub(crate) fn url_bad_scheme(url: Url) -> Error {

src/util.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::header::{Entry, HeaderMap, HeaderValue, OccupiedEntry};
2+
use std::fmt;
23

34
pub fn basic_auth<U, P>(username: U, password: Option<P>) -> HeaderValue
45
where
@@ -98,3 +99,44 @@ pub(crate) fn add_cookie_header(
9899
headers.insert(crate::header::COOKIE, header);
99100
}
100101
}
102+
103+
pub(crate) struct Escape<'a>(&'a [u8]);
104+
105+
#[cfg(not(target_arch = "wasm32"))]
106+
impl<'a> Escape<'a> {
107+
pub(crate) fn new(bytes: &'a [u8]) -> Self {
108+
Escape(bytes)
109+
}
110+
}
111+
112+
impl fmt::Debug for Escape<'_> {
113+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114+
write!(f, "b\"{}\"", self)?;
115+
Ok(())
116+
}
117+
}
118+
119+
impl fmt::Display for Escape<'_> {
120+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121+
for &c in self.0 {
122+
// https://doc.rust-lang.org/reference.html#byte-escapes
123+
if c == b'\n' {
124+
write!(f, "\\n")?;
125+
} else if c == b'\r' {
126+
write!(f, "\\r")?;
127+
} else if c == b'\t' {
128+
write!(f, "\\t")?;
129+
} else if c == b'\\' || c == b'"' {
130+
write!(f, "\\{}", c as char)?;
131+
} else if c == b'\0' {
132+
write!(f, "\\0")?;
133+
// ASCII printable
134+
} else if c >= 0x20 && c < 0x7f {
135+
write!(f, "{}", c as char)?;
136+
} else {
137+
write!(f, "\\x{c:02x}")?;
138+
}
139+
}
140+
Ok(())
141+
}
142+
}

tests/client.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
#![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))]
33
mod support;
44

5-
use support::server;
5+
use support::server::{self, low_level_with_response};
66

77
use http::header::{CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING};
88
#[cfg(feature = "json")]
99
use std::collections::HashMap;
1010

1111
use reqwest::Client;
12+
use tokio::io::AsyncWriteExt;
1213

1314
#[tokio::test]
1415
async fn auto_headers() {
@@ -564,3 +565,31 @@ async fn close_connection_after_idle_timeout() {
564565
.iter()
565566
.any(|e| matches!(e, server::Event::ConnectionClosed)));
566567
}
568+
569+
#[tokio::test]
570+
async fn http1_reason_phrase() {
571+
let server = server::low_level_with_response(|_raw_request, client_socket| {
572+
Box::new(async move {
573+
client_socket
574+
.write_all(b"HTTP/1.1 418 I'm not a teapot\r\nContent-Length: 0\r\n\r\n")
575+
.await
576+
.expect("response write_all failed");
577+
})
578+
});
579+
580+
let client = Client::new();
581+
582+
let res = client
583+
.get(&format!("http://{}", server.addr()))
584+
.send()
585+
.await
586+
.expect("Failed to get");
587+
588+
assert_eq!(
589+
res.error_for_status().unwrap_err().to_string(),
590+
format!(
591+
"HTTP status client error (418 I'm not a teapot) for url (http://{}/)",
592+
server.addr()
593+
)
594+
);
595+
}

0 commit comments

Comments
 (0)