Only retry idempotent requests. (#80)
This also reverts a change to send_body that was originally added to return the number of bytes written. It's no longer needed now that we check the size of the reader in advance. Fixes #76.
This commit is contained in:
committed by
GitHub
parent
15be014e05
commit
00461fb5bd
10
src/body.rs
10
src/body.rs
@@ -166,12 +166,12 @@ pub(crate) fn send_body(
|
|||||||
mut body: SizedReader,
|
mut body: SizedReader,
|
||||||
do_chunk: bool,
|
do_chunk: bool,
|
||||||
stream: &mut Stream,
|
stream: &mut Stream,
|
||||||
) -> IoResult<u64> {
|
) -> IoResult<()> {
|
||||||
let n = if do_chunk {
|
if do_chunk {
|
||||||
copy_chunked(&mut body.reader, stream)?
|
copy_chunked(&mut body.reader, stream)?;
|
||||||
} else {
|
} else {
|
||||||
copy(&mut body.reader, stream)?
|
copy(&mut body.reader, stream)?;
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(n)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use url::{form_urlencoded, Url};
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::agent::{self, Agent, AgentState};
|
use crate::agent::{self, Agent, AgentState};
|
||||||
use crate::body::Payload;
|
use crate::body::{Payload, SizedReader};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::header::{self, Header};
|
use crate::header::{self, Header};
|
||||||
use crate::pool;
|
use crate::pool;
|
||||||
@@ -605,6 +605,22 @@ impl Request {
|
|||||||
self.tls_config = Some(TLSClientConfig(tls_config));
|
self.tls_config = Some(TLSClientConfig(tls_config));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if this request, with the provided body, is retryable.
|
||||||
|
pub(crate) fn is_retryable(&self, body: &SizedReader) -> bool {
|
||||||
|
// Per https://tools.ietf.org/html/rfc7231#section-8.1.3
|
||||||
|
// these methods are idempotent.
|
||||||
|
let idempotent = match self.method.as_str() {
|
||||||
|
"DELETE" | "GET" | "HEAD" | "OPTIONS" | "PUT" | "TRACE" => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
// Unsized bodies aren't retryable because we can't rewind the reader.
|
||||||
|
// Sized bodies are retryable only if they are zero-length because of
|
||||||
|
// coincidences of the current implementation - the function responsible
|
||||||
|
// for retries doesn't have a way to replay a Payload.
|
||||||
|
let no_body = body.size.is_none() || body.size.unwrap() > 0;
|
||||||
|
idempotent && no_body
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
use crate::test;
|
|
||||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|||||||
23
src/unit.rs
23
src/unit.rs
@@ -155,21 +155,30 @@ pub(crate) fn connect(
|
|||||||
return Err(err.into());
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let retryable = req.is_retryable(&body);
|
||||||
|
|
||||||
// send the body (which can be empty now depending on redirects)
|
// send the body (which can be empty now depending on redirects)
|
||||||
let body_bytes_sent = body::send_body(body, unit.is_chunked, &mut stream)?;
|
body::send_body(body, unit.is_chunked, &mut stream)?;
|
||||||
|
|
||||||
// start reading the response to process cookies and redirects.
|
// start reading the response to process cookies and redirects.
|
||||||
let mut stream = stream::DeadlineStream::new(stream, unit.deadline);
|
let mut stream = stream::DeadlineStream::new(stream, unit.deadline);
|
||||||
let mut resp = Response::from_read(&mut stream);
|
let mut resp = Response::from_read(&mut stream);
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7230#section-6.3.1
|
||||||
|
// When an inbound connection is closed prematurely, a client MAY
|
||||||
|
// open a new connection and automatically retransmit an aborted
|
||||||
|
// sequence of requests if all of those requests have idempotent
|
||||||
|
// methods.
|
||||||
|
//
|
||||||
|
// We choose to retry only once. To do that, we rely on is_recycled,
|
||||||
|
// the "one connection per hostname" police of the ConnectionPool,
|
||||||
|
// and the fact that connections with errors are dropped.
|
||||||
|
//
|
||||||
|
// 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() && body_bytes_sent == 0 && is_recycled {
|
if err.is_bad_status_read() && retryable && is_recycled {
|
||||||
// We try open a new connection, this happens if the remote server
|
|
||||||
// hangs a pooled connection and we only discover when trying to
|
|
||||||
// read from it. It's however only possible if we didn't send any
|
|
||||||
// body bytes. This is because we currently don't want to buffer
|
|
||||||
// any body to be able to replay it.
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user