Handle ConnectionReset+ConnectionAbort at any time (#168)

Previously we had a special case for BadStatusRead that would happen
only when we got a ConnectionAborted error reading the status line.
However, sometimes we get ConnectionReset instead. Also the HTTP
spec says that idempotent requests may be retried anytime a connection
is closed prematurely.

The change treats as retryable any ConnectionAborted OR ConnectionReset
error while reading the status line and headers. It removes the special
case BadStatusRead error.

Fixes #165 (I think).
This commit is contained in:
Jacob Hoffman-Andrews
2020-09-29 01:55:34 -07:00
committed by GitHub
parent 065b560dfb
commit 17d7e147eb
4 changed files with 10 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
use std::fmt; use std::fmt;
use std::io; use std::io::{self, ErrorKind};
/// Errors that are translated to ["synthetic" responses](struct.Response.html#method.synthetic). /// Errors that are translated to ["synthetic" responses](struct.Response.html#method.synthetic).
#[derive(Debug)] #[derive(Debug)]
@@ -14,9 +14,6 @@ pub enum Error {
ConnectionFailed(String), ConnectionFailed(String),
/// Too many redirects. Synthetic error `500`. /// Too many redirects. Synthetic error `500`.
TooManyRedirects, TooManyRedirects,
/// We fail to read the status line. This happens for pooled connections when
/// TLS fails and we don't notice until trying to read.
BadStatusRead,
/// A status line we don't understand `HTTP/1.1 200 OK`. Synthetic error `500`. /// A status line we don't understand `HTTP/1.1 200 OK`. Synthetic error `500`.
BadStatus, BadStatus,
/// A header line that couldn't be parsed. Synthetic error `500`. /// A header line that couldn't be parsed. Synthetic error `500`.
@@ -37,11 +34,11 @@ pub enum Error {
} }
impl Error { impl Error {
// If the error is bad status read, which might happen if a TLS connections is // Return true iff the error was due to a connection closing.
// closed and we only discover it when trying to read the status line from it. pub(crate) fn connection_closed(&self) -> bool {
pub(crate) fn is_bad_status_read(&self) -> bool {
match self { match self {
Error::BadStatusRead => true, Error::Io(e) if e.kind() == ErrorKind::ConnectionAborted => true,
Error::Io(e) if e.kind() == ErrorKind::ConnectionReset => true,
_ => false, _ => false,
} }
} }
@@ -54,7 +51,6 @@ impl Error {
Error::DnsFailed(_) => 400, Error::DnsFailed(_) => 400,
Error::ConnectionFailed(_) => 500, Error::ConnectionFailed(_) => 500,
Error::TooManyRedirects => 500, Error::TooManyRedirects => 500,
Error::BadStatusRead => 500,
Error::BadStatus => 500, Error::BadStatus => 500,
Error::BadHeader => 500, Error::BadHeader => 500,
Error::Io(_) => 500, Error::Io(_) => 500,
@@ -75,7 +71,6 @@ impl Error {
Error::DnsFailed(_) => "Dns Failed", Error::DnsFailed(_) => "Dns Failed",
Error::ConnectionFailed(_) => "Connection Failed", Error::ConnectionFailed(_) => "Connection Failed",
Error::TooManyRedirects => "Too Many Redirects", Error::TooManyRedirects => "Too Many Redirects",
Error::BadStatusRead => "Failed to read status line",
Error::BadStatus => "Bad Status", Error::BadStatus => "Bad Status",
Error::BadHeader => "Bad Header", Error::BadHeader => "Bad Header",
Error::Io(_) => "Network Error", Error::Io(_) => "Network Error",
@@ -96,7 +91,6 @@ impl Error {
Error::DnsFailed(err) => format!("Dns Failed: {}", err), Error::DnsFailed(err) => format!("Dns Failed: {}", err),
Error::ConnectionFailed(err) => format!("Connection Failed: {}", err), Error::ConnectionFailed(err) => format!("Connection Failed: {}", err),
Error::TooManyRedirects => "Too Many Redirects".to_string(), Error::TooManyRedirects => "Too Many Redirects".to_string(),
Error::BadStatusRead => "Failed to read status line".to_string(),
Error::BadStatus => "Bad Status".to_string(), Error::BadStatus => "Bad Status".to_string(),
Error::BadHeader => "Bad Header".to_string(), Error::BadHeader => "Bad Header".to_string(),
Error::Io(ioe) => format!("Network Error: {}", ioe), Error::Io(ioe) => format!("Network Error: {}", ioe),

View File

@@ -476,10 +476,7 @@ impl Response {
fn do_from_read(mut reader: impl Read) -> Result<Response, Error> { fn do_from_read(mut reader: impl Read) -> Result<Response, Error> {
// //
// HTTP/1.1 200 OK\r\n // HTTP/1.1 200 OK\r\n
let status_line = read_next_line(&mut reader).map_err(|e| match e.kind() { let status_line = read_next_line(&mut reader)?;
ErrorKind::ConnectionAborted => Error::BadStatusRead,
_ => Error::Io(e),
})?;
let (index, status) = parse_status_line(status_line.as_str())?; let (index, status) = parse_status_line(status_line.as_str())?;

View File

@@ -368,7 +368,9 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error
.connect(&hostname.trim_matches(|c| c == '[' || c == ']'), sock) .connect(&hostname.trim_matches(|c| c == '[' || c == ']'), sock)
.map_err(|e| match e { .map_err(|e| match e {
HandshakeError::Failure(err) => Error::TlsError(err), HandshakeError::Failure(err) => Error::TlsError(err),
_ => Error::BadStatusRead, // The only other possibility is WouldBlock. Since we don't
// handle retries of WouldBlock, turn it into a generic error.
_ => Error::ConnectionFailed("TLS handshake unexpected error".to_string()),
})?; })?;
Ok(Stream::Https(BufReader::new(stream))) Ok(Stream::Https(BufReader::new(stream)))
@@ -380,7 +382,6 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
} else { } else {
unit.deadline unit.deadline
}; };
let netloc = match unit.req.proxy { let netloc = match unit.req.proxy {
Some(ref proxy) => format!("{}:{}", proxy.server, proxy.port), Some(ref proxy) => format!("{}:{}", proxy.server, proxy.port),
None => format!("{}:{}", hostname, port), None => format!("{}:{}", hostname, port),

View File

@@ -189,12 +189,8 @@ pub(crate) fn connect(
// from the ConnectionPool, since those are most likely to have // from the ConnectionPool, since those are most likely to have
// reached a server-side timeout. Note that this means we may do // reached a server-side timeout. Note that this means we may do
// up to N+1 total tries, where N is max_idle_connections_per_host. // up to N+1 total tries, where N is max_idle_connections_per_host.
//
// TODO: is_bad_status_read is too narrow since it covers only the
// first line. It's also allowable to retry requests that hit a
// closed connection during the sending or receiving of headers.
if let Some(err) = resp.synthetic_error() { if let Some(err) = resp.synthetic_error() {
if err.is_bad_status_read() && retryable && is_recycled { if err.connection_closed() && retryable && is_recycled {
debug!("retrying request {} {}", method, url); debug!("retrying request {} {}", method, url);
let empty = Payload::Empty.into_read(); let empty = Payload::Empty.into_read();
return connect(req, unit, false, redirect_count, empty, redir); return connect(req, unit, false, redirect_count, empty, redir);