Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(573)

Unified Diff: net/http/http_network_transaction.cc

Issue 403393003: HTTP retry support. Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fixes according to review. Created 6 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « net/http/http_network_transaction.h ('k') | net/http/http_network_transaction_unittest.cc » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: net/http/http_network_transaction.cc
diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc
index f4df073b4bba5ad905631653d2cba46820c0daa7..1f05ea418c1b1ddf3e4112522472fd036333d5c7 100644
--- a/net/http/http_network_transaction.cc
+++ b/net/http/http_network_transaction.cc
@@ -15,6 +15,7 @@
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram.h"
#include "base/metrics/stats_counters.h"
+#include "base/pickle.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
@@ -22,6 +23,8 @@
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
+#include "crypto/secure_hash.h"
+#include "crypto/sha2.h"
#include "net/base/auth.h"
#include "net/base/host_port_pair.h"
#include "net/base/io_buffer.h"
@@ -75,6 +78,8 @@ namespace net {
namespace {
+const int kNumRetries = 3;
+
void ProcessAlternateProtocol(
HttpNetworkSession* session,
const HttpResponseHeaders& headers,
@@ -128,6 +133,61 @@ base::Value* NetLogSSLVersionFallbackCallback(
//-----------------------------------------------------------------------------
+// To verify that retry attempts will not cause errors we hash all received
+// content. When retrying we hash the content again and verify that the
+// previous hash matches once we have received the same amount of data.
+class HttpNetworkTransaction::HttpStreamHash {
+public:
+ HttpStreamHash()
+ :hash_(crypto::SecureHash::Create(crypto::SecureHash::SHA256)) {
+ }
+
+ // Add to hash.
+ void Update(const void* input, size_t len) {
+ hash_->Update(input, len);
+ }
+
+ // Finish hash once all content has been received.
+ void Finish() {
+ hash_->Finish(previous_hash_, sizeof(previous_hash_));
+ hash_.reset(crypto::SecureHash::Create(crypto::SecureHash::SHA256));
+ }
+
+ // Verify hash after serializing the state. Then deserialize so that we can
+ // keep hashing if we decide to continue fetching the content.
+ int VerifyHash() {
+ int result = OK;
+ uint8 hash[8];
+ Pickle pickle;
+ hash_->Serialize(&pickle);
+ hash_->Finish(hash, sizeof(hash));
+ DCHECK(sizeof(hash) == sizeof(previous_hash_));
+ if (memcmp(hash, previous_hash_, sizeof(previous_hash_)) != 0) {
+ result = ERR_RETRY_HASH_MISMATCH;
+ UMA_HISTOGRAM_COUNTS("Net.HttpRetry.VerifyHashFailure", 1);
+ }
+ else {
+ PickleIterator data_iterator(pickle);
+ hash_->Deserialize(&data_iterator);
+ UMA_HISTOGRAM_COUNTS("Net.HttpRetry.VerifyHashSuccess", 1);
+ }
+
+ return result;
+ }
+
+ void SetPreviousHash(const void* hash, size_t len) {
+ DCHECK(len == sizeof(previous_hash_));
+ memcpy(previous_hash_, hash, len);
+ }
+
+private:
+ // Hash of current attempt to retrieve the resource.
+ scoped_ptr<crypto::SecureHash> hash_;
+
+ // Hash of previous attempt to retrieve the resource.
+ uint8 previous_hash_[8];
+};
+
HttpNetworkTransaction::HttpNetworkTransaction(RequestPriority priority,
HttpNetworkSession* session)
: pending_auth_target_(HttpAuth::AUTH_NONE),
@@ -142,6 +202,11 @@ HttpNetworkTransaction::HttpNetworkTransaction(RequestPriority priority,
request_headers_(),
read_buf_len_(0),
total_received_bytes_(0),
+ retry_attempt_(0),
+ offset_(0),
+ previous_content_length_(0),
+ received_body_length_(0),
+ stream_hash_(new HttpStreamHash()),
next_state_(STATE_NONE),
establishing_tunnel_(false),
websocket_handshake_stream_base_create_helper_(NULL) {
@@ -1000,6 +1065,23 @@ int HttpNetworkTransaction::DoReadHeadersComplete(int result) {
DCHECK(response_.headers.get());
+ if ((response_.headers->response_code() == 200 ||
+ response_.headers->response_code() == 206) && offset_) {
+ std::string etag, last_modified;
+ response_.headers->EnumerateHeader(NULL, "last-modified", &last_modified);
+ if (response_.headers->GetHttpVersion() >= HttpVersion(1, 1))
+ response_.headers->EnumerateHeader(NULL, "etag", &etag);
+
+ // Return an error if content was updated on a retry.
+ if ((previous_content_length_ != response_.headers->GetContentLength() &&
+ response_.headers->response_code() == 200) ||
+ previous_etag_ != etag || previous_last_modified_ != last_modified) {
+ return HandleIOError(ERR_RETRY_CONTENT_UPDATED);
+ }
+ }
+ if (response_.headers->response_code() != 200)
+ offset_ = 0;
+
// On a 408 response from the server ("Request Timeout") on a stale socket,
// retry the request.
// Headers can be NULL because of http://crbug.com/384554.
@@ -1061,6 +1143,9 @@ int HttpNetworkTransaction::DoReadHeadersComplete(int result) {
headers_valid_ = true;
+ if (retry_attempt_ && read_buf_.get())
+ next_state_ = STATE_READ_BODY;
+
if (session_->huffman_aggregator()) {
session_->huffman_aggregator()->AggregateTransactionCharacterCounts(
*request_,
@@ -1086,6 +1171,9 @@ int HttpNetworkTransaction::DoReadBodyComplete(int result) {
bool done = false;
if (result <= 0) {
DCHECK_NE(ERR_IO_PENDING, result);
+ stream_hash_->Finish();
+ if (result < 0)
+ return HandleIOError(result);
done = true;
}
@@ -1107,6 +1195,36 @@ int HttpNetworkTransaction::DoReadBodyComplete(int result) {
}
}
+ if (result > 0) {
+ received_body_length_ += result;
+ if (offset_) {
+ if (offset_ <= result) {
+ stream_hash_->Update((uint8*)read_buf_->data(), offset_);
+ if (stream_hash_->VerifyHash() != OK)
+ return HandleIOError(ERR_RETRY_HASH_MISMATCH);
+ stream_hash_->Update((uint8*)read_buf_->data() + offset_,
+ result - offset_);
+ memmove(read_buf_->data(), read_buf_->data() + offset_,
+ result - offset_);
+ result -= offset_;
+ offset_ = 0;
+ }
+ else
+ {
+ stream_hash_->Update((uint8*)read_buf_->data(), result);
+ offset_ -= result;
+ result = 0;
+ }
+
+ if (result == 0) {
+ next_state_ = STATE_READ_BODY;
+ return result;
+ }
+ }
+ else
+ stream_hash_->Update((uint8*)read_buf_->data(), result);
+ }
+
// Clean up connection if we are done.
if (done) {
LogTransactionMetrics();
@@ -1227,6 +1345,21 @@ void HttpNetworkTransaction::LogTransactionMetrics() const {
}
}
+void HttpNetworkTransaction::LogTransactionRetryMetrics(int error) const {
+ UMA_HISTOGRAM_CUSTOM_COUNTS("Net.HttpRetry.Count", retry_attempt_, 1, 5, 5);
+ if (request_->load_flags & LOAD_MAIN_FRAME) {
+ UMA_HISTOGRAM_CUSTOM_ENUMERATION(
+ "Net.HttpRetry.Cause.MainFrame",
+ -error,
+ GetAllErrorCodesForUma());
+ } else {
+ UMA_HISTOGRAM_CUSTOM_ENUMERATION(
+ "Net.HttpRetry.Cause.Subresource",
+ -error,
+ GetAllErrorCodesForUma());
+ }
+}
+
int HttpNetworkTransaction::HandleCertificateRequest(int error) {
// There are two paths through which the server can request a certificate
// from us. The first is during the initial handshake, the second is
@@ -1421,6 +1554,8 @@ int HttpNetworkTransaction::HandleIOError(int error) {
if (ShouldResendRequest()) {
net_log_.AddEventWithNetErrorCode(
NetLog::TYPE_HTTP_TRANSACTION_RESTART_AFTER_ERROR, error);
+ retry_attempt_++;
+ LogTransactionRetryMetrics(error);
ResetConnectionAndRequestForResend();
error = OK;
}
@@ -1462,27 +1597,99 @@ HttpResponseHeaders* HttpNetworkTransaction::GetResponseHeaders() const {
}
bool HttpNetworkTransaction::ShouldResendRequest() const {
- bool connection_is_proven = stream_->IsConnectionReused();
- bool has_received_headers = GetResponseHeaders() != NULL;
+ if (!stream_->IsConnectionReused() &&
+ !(request_->method == "GET" || request_->method == "HEAD"))
rvargas (doing something else) 2014/07/26 00:30:33 nit: requires {}
+ return false;
- // NOTE: we resend a request only if we reused a keep-alive connection.
- // This automatically prevents an infinite resend loop because we'll run
- // out of the cached keep-alive connections eventually.
- if (connection_is_proven && !has_received_headers)
- return true;
- return false;
-}
+ if (retry_attempt_ > kNumRetries)
+ return false;
-void HttpNetworkTransaction::ResetConnectionAndRequestForResend() {
- if (stream_.get()) {
- stream_->Close(true);
- stream_.reset();
+ if (GetResponseHeaders() != NULL) {
+ // Do not retry automatically if resource is larger than 1 Mb until byte
+ // range support is enabled. This is temporary.
+ if (response_.headers->GetContentLength() > 1000000)
+ return false;
+
+ TimeDelta time_delta = base::TimeTicks::Now() - send_start_time_;
+ if (time_delta.InSeconds() > 30)
+ return false;
+
+ if (response_.headers->HasStrongValidators()) {
rvargas (doing something else) 2014/07/26 00:30:33 nit: no {}
+ return true;
+ }
+
+ if (response_.headers->HasHeaderValue("cache-control", "no-cache") ||
+ response_.headers->HasHeaderValue("cache-control", "no-store") ||
+ response_.headers->HasHeaderValue("cache-control", "must-revalidate") ||
+ response_.headers->HasHeaderValue("pragma", "no-cache")) {
+ return false;
+ }
+
+ if (response_.headers->GetMaxAgeValue(&time_delta))
+ return time_delta.InSeconds() > 60;
+
+ // If there is no Date header, then assume that the server response was
+ // generated at the time when we received the response, hence do not retry.
+ Time date_value;
+ if (!response_.headers->GetDateValue(&date_value))
+ return false;
+
+ Time expires_value;
+ if (response_.headers->GetExpiresValue(&expires_value)) {
+ time_delta = expires_value - date_value;
+ if (time_delta.InSeconds() > 60)
+ return true;
+ }
+ return false;
}
+ return true;
+}
+void HttpNetworkTransaction::ResetConnectionAndRequestForResend() {
// We need to clear request_headers_ because it contains the real request
// headers, but we may need to resend the CONNECT request first to recreate
// the SSL tunnel.
request_headers_.Clear();
+ headers_valid_ = false;
+
+ if (stream_) {
+ total_received_bytes_ += stream_->GetTotalReceivedBytes();
+ if (received_body_length_ > offset_) {
+ offset_ = received_body_length_;
+ }
+ }
+ received_body_length_ = 0;
+
+ HttpResponseHeaders* headers = GetResponseHeaders();
+ if (offset_ && headers) {
+ previous_content_length_ = headers->GetContentLength();
+ headers->EnumerateHeader(NULL, "last-modified",
+ &previous_last_modified_);
+ if (headers->GetHttpVersion() >= HttpVersion(1, 1))
+ headers->EnumerateHeader(NULL, "etag", &previous_etag_);
+
+ // Disable until we have statistics that show that retrying does not
+ // result in too many ERR_RETRY_HASH_MISMATCH.
+ if (0 && previous_content_length_ &&
+ headers->HasHeaderValue("Accept-Ranges", "bytes")) {
+ // Try resending using a range request if supported.
+ request_headers_.SetHeader(HttpRequestHeaders::kRange,
+ "bytes=" + base::Uint64ToString(offset_) + "-");
+ if (!previous_etag_.empty())
+ request_headers_.SetHeader("If-Match", previous_etag_);
+ else if (!previous_last_modified_.empty()) {
+ request_headers_.SetHeader("If-Unmodified-Since",
+ previous_last_modified_);
+ }
+ }
+ }
+
+ response_ = HttpResponseInfo();
+ establishing_tunnel_ = false;
+ if (stream_.get()) {
+ stream_->Close(true);
+ stream_.reset();
+ }
next_state_ = STATE_CREATE_STREAM; // Resend the request.
}
« no previous file with comments | « net/http/http_network_transaction.h ('k') | net/http/http_network_transaction_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698