diff --git a/net/base/load_flags_list.h b/net/base/load_flags_list.h index a342d7615807c3..918cdb803fd33d 100644 --- a/net/base/load_flags_list.h +++ b/net/base/load_flags_list.h @@ -92,3 +92,13 @@ LOAD_FLAG(DISABLE_CONNECTION_MIGRATION, 1 << 16) // Indicates that the cache should not check that the request matches the // response's vary header. LOAD_FLAG(SKIP_VARY_CHECK, 1 << 17) + +// The creator of this URLRequest wishes to receive stale responses when allowed +// by the "Cache-Control: stale-while-revalidate" directive and is able to issue +// an async revalidation to update the cache. If the callee needs to revalidate +// the resource |async_revalidation_requested| attribute will be set on the +// associated HttpResponseInfo. If indicated the callee should revalidate the +// resource by issuing a new request without this flag set. If the revalidation +// does not complete in 60 seconds, the cache treat the stale resource as +// invalid, as it did not specify stale-while-revalidate. +LOAD_FLAG(SUPPORT_ASYNC_REVALIDATION, 1 << 18) diff --git a/net/http/http_cache_transaction.cc b/net/http/http_cache_transaction.cc index efb97f756eae48..03e3aa73bd7ea1 100644 --- a/net/http/http_cache_transaction.cc +++ b/net/http/http_cache_transaction.cc @@ -18,16 +18,20 @@ #include "base/bind_helpers.h" #include "base/callback_helpers.h" #include "base/compiler_specific.h" +#include "base/format_macros.h" #include "base/location.h" #include "base/macros.h" #include "base/metrics/histogram_functions.h" #include "base/metrics/histogram_macros.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_number_conversions.h" // For HexEncode. +#include "base/strings/string_piece.h" #include "base/strings/string_util.h" // For LowerCaseEqualsASCII. +#include "base/strings/stringprintf.h" #include "base/threading/thread_task_runner_handle.h" #include "base/time/clock.h" #include "base/trace_event/trace_event.h" +#include "base/values.h" #include "net/base/auth.h" #include "net/base/load_flags.h" #include "net/base/load_timing_info.h" @@ -54,6 +58,8 @@ using CacheEntryStatus = HttpResponseInfo::CacheEntryStatus; namespace { +constexpr TimeDelta kStaleRevalidateTimeout = TimeDelta::FromSeconds(60); + // From http://tools.ietf.org/html/draft-ietf-httpbis-p6-cache-21#section-6 // a "non-error response" is one with a 2xx (Successful) or 3xx // (Redirection) status code. @@ -71,6 +77,13 @@ void RecordNoStoreHeaderHistogram(int load_flags, } } +enum ExternallyConditionalizedType { + EXTERNALLY_CONDITIONALIZED_CACHE_REQUIRES_VALIDATION, + EXTERNALLY_CONDITIONALIZED_CACHE_USABLE, + EXTERNALLY_CONDITIONALIZED_MISMATCHED_VALIDATORS, + EXTERNALLY_CONDITIONALIZED_MAX +}; + } // namespace #define CACHE_STATUS_HISTOGRAMS(type) \ @@ -897,6 +910,17 @@ int HttpCache::Transaction::DoLoop(int result) { case STATE_COMPLETE_PARTIAL_CACHE_VALIDATION: rv = DoCompletePartialCacheValidation(rv); break; + case STATE_CACHE_UPDATE_STALE_WHILE_REVALIDATE_TIMEOUT: + DCHECK_EQ(OK, rv); + rv = DoCacheUpdateStaleWhileRevalidateTimeout(); + break; + case STATE_CACHE_UPDATE_STALE_WHILE_REVALIDATE_TIMEOUT_COMPLETE: + rv = DoCacheUpdateStaleWhileRevalidateTimeoutComplete(rv); + break; + case STATE_SETUP_ENTRY_FOR_READ: + DCHECK_EQ(OK, rv); + rv = DoSetupEntryForRead(); + break; case STATE_SEND_REQUEST: DCHECK_EQ(OK, rv); rv = DoSendRequest(); @@ -1592,6 +1616,24 @@ int HttpCache::Transaction::DoCompletePartialCacheValidation(int result) { return BeginCacheValidation(); } +int HttpCache::Transaction::DoCacheUpdateStaleWhileRevalidateTimeout() { + TRACE_EVENT0( + "io", "HttpCacheTransaction::DoCacheUpdateStaleWhileRevalidateTimeout"); + response_.stale_revalidate_timeout = + cache_->clock_->Now() + kStaleRevalidateTimeout; + TransitionToState(STATE_CACHE_UPDATE_STALE_WHILE_REVALIDATE_TIMEOUT_COMPLETE); + return WriteResponseInfoToEntry(false); +} + +int HttpCache::Transaction::DoCacheUpdateStaleWhileRevalidateTimeoutComplete( + int result) { + TRACE_EVENT0( + "io", + "HttpCacheTransaction::DoCacheUpdateStaleWhileRevalidateTimeoutComplete"); + TransitionToState(STATE_SETUP_ENTRY_FOR_READ); + return OnWriteResponseInfoToEntryComplete(result); +} + int HttpCache::Transaction::DoSendRequest() { TRACE_EVENT0("io", "HttpCacheTransaction::DoSendRequest"); DCHECK(mode_ & WRITE || mode_ == NONE); @@ -1779,6 +1821,7 @@ int HttpCache::Transaction::DoUpdateCachedResponse() { // Update the cached response based on the headers and properties of // new_response_. response_.headers->Update(*new_response_->headers.get()); + response_.stale_revalidate_timeout = base::Time(); response_.response_time = new_response_->response_time; response_.request_time = new_response_->request_time; response_.network_accessed = new_response_->network_accessed; @@ -2410,7 +2453,7 @@ int HttpCache::Transaction::BeginCacheRead() { return ERR_CACHE_MISS; } - if (RequiresValidation()) { + if (RequiresValidation() != VALIDATION_NONE) { TransitionToState(STATE_FINISH_HEADERS); return ERR_CACHE_MISS; } @@ -2429,13 +2472,27 @@ int HttpCache::Transaction::BeginCacheRead() { int HttpCache::Transaction::BeginCacheValidation() { DCHECK_EQ(mode_, READ_WRITE); - bool skip_validation = !RequiresValidation(); + ValidationType required_validation = RequiresValidation(); + + bool skip_validation = (required_validation == VALIDATION_NONE); + bool needs_stale_while_revalidate_cache_update = false; + + if ((effective_load_flags_ & LOAD_SUPPORT_ASYNC_REVALIDATION) && + required_validation == VALIDATION_ASYNCHRONOUS) { + DCHECK_EQ(request_->method, "GET"); + skip_validation = true; + response_.async_revalidation_requested = true; + needs_stale_while_revalidate_cache_update = + response_.stale_revalidate_timeout.is_null(); + } if (method_ == "HEAD" && (truncated_ || response_.headers->response_code() == 206)) { DCHECK(!partial_); - if (skip_validation) - return SetupEntryForRead(); + if (skip_validation) { + TransitionToState(STATE_SETUP_ENTRY_FOR_READ); + return OK; + } // Bail out! TransitionToState(STATE_SEND_REQUEST); @@ -2460,7 +2517,10 @@ int HttpCache::Transaction::BeginCacheValidation() { if (skip_validation) { UpdateCacheEntryStatus(CacheEntryStatus::ENTRY_USED); - return SetupEntryForRead(); + TransitionToState(needs_stale_while_revalidate_cache_update + ? STATE_CACHE_UPDATE_STALE_WHILE_REVALIDATE_TIMEOUT + : STATE_SETUP_ENTRY_FOR_READ); + return OK; } else { // Make the network request conditional, to see if we may reuse our cached // response. If we cannot do so, then we just resort to a normal fetch. @@ -2596,7 +2656,7 @@ int HttpCache::Transaction::RestartNetworkRequestWithAuth( return rv; } -bool HttpCache::Transaction::RequiresValidation() { +ValidationType HttpCache::Transaction::RequiresValidation() { // TODO(darin): need to do more work here: // - make sure we have a matching request method // - watch out for cached responses that depend on authentication @@ -2607,11 +2667,11 @@ bool HttpCache::Transaction::RequiresValidation() { *response_.headers.get())) { vary_mismatch_ = true; validation_cause_ = VALIDATION_CAUSE_VARY_MISMATCH; - return true; + return VALIDATION_SYNCHRONOUS; } if (effective_load_flags_ & LOAD_SKIP_CACHE_VALIDATION) - return false; + return VALIDATION_NONE; if (response_.unused_since_prefetch && !(effective_load_flags_ & LOAD_PREFETCH) && @@ -2620,21 +2680,23 @@ bool HttpCache::Transaction::RequiresValidation() { cache_->clock_->Now()) < TimeDelta::FromMinutes(kPrefetchReuseMins)) { // The first use of a resource after prefetch within a short window skips // validation. - return false; + return VALIDATION_NONE; } if (effective_load_flags_ & LOAD_VALIDATE_CACHE) { validation_cause_ = VALIDATION_CAUSE_VALIDATE_FLAG; - return true; + return VALIDATION_SYNCHRONOUS; } if (method_ == "PUT" || method_ == "DELETE") - return true; + return VALIDATION_SYNCHRONOUS; - bool validation_required_by_headers = response_.headers->RequiresValidation( - response_.request_time, response_.response_time, cache_->clock_->Now()); + ValidationType validation_required_by_headers = + response_.headers->RequiresValidation(response_.request_time, + response_.response_time, + cache_->clock_->Now()); - if (validation_required_by_headers) { + if (validation_required_by_headers != VALIDATION_NONE) { HttpResponseHeaders::FreshnessLifetimes lifetimes = response_.headers->GetFreshnessLifetimes(response_.response_time); if (lifetimes.freshness == base::TimeDelta()) { @@ -2648,6 +2710,19 @@ bool HttpCache::Transaction::RequiresValidation() { } } + if (validation_required_by_headers == VALIDATION_ASYNCHRONOUS) { + // Asynchronous revalidation is only supported for GET methods. + if (request_->method != "GET") + return VALIDATION_SYNCHRONOUS; + + // If the timeout on the staleness revalidation is set don't hand out + // a resource that hasn't been async validated. + if (!response_.stale_revalidate_timeout.is_null() && + response_.stale_revalidate_timeout < cache_->clock_->Now()) { + return VALIDATION_SYNCHRONOUS; + } + } + return validation_required_by_headers; } @@ -2922,7 +2997,7 @@ void HttpCache::Transaction::FixHeadersForHead() { } } -int HttpCache::Transaction::SetupEntryForRead() { +int HttpCache::Transaction::DoSetupEntryForRead() { if (network_trans_) ResetNetworkTransaction(); if (partial_) { diff --git a/net/http/http_cache_transaction.h b/net/http/http_cache_transaction.h index 3a67dfd987025c..e0252c46ccdb37 100644 --- a/net/http/http_cache_transaction.h +++ b/net/http/http_cache_transaction.h @@ -256,6 +256,9 @@ class NET_EXPORT_PRIVATE HttpCache::Transaction : public HttpTransaction { STATE_CACHE_QUERY_DATA_COMPLETE, STATE_START_PARTIAL_CACHE_VALIDATION, STATE_COMPLETE_PARTIAL_CACHE_VALIDATION, + STATE_CACHE_UPDATE_STALE_WHILE_REVALIDATE_TIMEOUT, + STATE_CACHE_UPDATE_STALE_WHILE_REVALIDATE_TIMEOUT_COMPLETE, + STATE_SETUP_ENTRY_FOR_READ, STATE_SEND_REQUEST, STATE_SEND_REQUEST_COMPLETE, STATE_SUCCESSFUL_SEND_REQUEST, @@ -333,6 +336,9 @@ class NET_EXPORT_PRIVATE HttpCache::Transaction : public HttpTransaction { int DoCacheDispatchValidation(); int DoCacheQueryData(); int DoCacheQueryDataComplete(int result); + int DoCacheUpdateStaleWhileRevalidateTimeout(); + int DoCacheUpdateStaleWhileRevalidateTimeoutComplete(int result); + int DoSetupEntryForRead(); int DoStartPartialCacheValidation(); int DoCompletePartialCacheValidation(int result); int DoSendRequest(); @@ -407,8 +413,9 @@ class NET_EXPORT_PRIVATE HttpCache::Transaction : public HttpTransaction { // Returns network error code. int RestartNetworkRequestWithAuth(const AuthCredentials& credentials); - // Called to determine if we need to validate the cache entry before using it. - bool RequiresValidation(); + // Called to determine if we need to validate the cache entry before using it, + // and whether the validation should be synchronous or asynchronous. + ValidationType RequiresValidation(); // Called to make the request conditional (to ask the server if the cached // copy is valid). Returns true if able to make the request conditional. @@ -443,9 +450,6 @@ class NET_EXPORT_PRIVATE HttpCache::Transaction : public HttpTransaction { // Fixes the response headers to match expectations for a HEAD request. void FixHeadersForHead(); - // Setups the transaction for reading from the cache entry. - int SetupEntryForRead(); - // Called to write data to the cache entry. If the write fails, then the // cache entry is destroyed. Future calls to this function will just do // nothing without side-effect. Returns a network error code. diff --git a/net/http/http_cache_unittest.cc b/net/http/http_cache_unittest.cc index c42178d89366d0..abe9a0ad1b5e26 100644 --- a/net/http/http_cache_unittest.cc +++ b/net/http/http_cache_unittest.cc @@ -10311,6 +10311,154 @@ TEST_F(HttpCachePrefetchValidationTest, ValidateOnDelayedSecondPrefetch) { EXPECT_FALSE(TransactionRequiredNetwork(LOAD_NORMAL)); } +TEST_F(HttpCacheTest, StaleContentNotUsedWhenLoadFlagNotSet) { + MockHttpCache cache; + + ScopedMockTransaction stale_while_revalidate_transaction( + kSimpleGET_Transaction); + + stale_while_revalidate_transaction.response_headers = + "Last-Modified: Sat, 18 Apr 2007 01:10:43 GMT\n" + "Age: 10801\n" + "Cache-Control: max-age=0,stale-while-revalidate=86400\n"; + + // Write to the cache. + RunTransactionTest(cache.http_cache(), stale_while_revalidate_transaction); + + EXPECT_EQ(1, cache.network_layer()->transaction_count()); + + // Send the request again and check that it is sent to the network again. + HttpResponseInfo response_info; + RunTransactionTestWithResponseInfo( + cache.http_cache(), stale_while_revalidate_transaction, &response_info); + + EXPECT_EQ(2, cache.network_layer()->transaction_count()); + EXPECT_FALSE(response_info.async_revalidation_requested); +} + +TEST_F(HttpCacheTest, StaleContentUsedWhenLoadFlagSetAndUsableThenTimesout) { + MockHttpCache cache; + base::SimpleTestClock clock; + cache.http_cache()->SetClockForTesting(&clock); + cache.network_layer()->SetClock(&clock); + clock.Advance(base::TimeDelta::FromSeconds(10)); + + ScopedMockTransaction stale_while_revalidate_transaction( + kSimpleGET_Transaction); + stale_while_revalidate_transaction.load_flags |= + LOAD_SUPPORT_ASYNC_REVALIDATION; + stale_while_revalidate_transaction.response_headers = + "Last-Modified: Sat, 18 Apr 2007 01:10:43 GMT\n" + "Age: 10801\n" + "Cache-Control: max-age=0,stale-while-revalidate=86400\n"; + + // Write to the cache. + RunTransactionTest(cache.http_cache(), stale_while_revalidate_transaction); + + EXPECT_EQ(1, cache.network_layer()->transaction_count()); + + // Send the request again and check that it is not sent to the network again. + HttpResponseInfo response_info; + RunTransactionTestWithResponseInfo( + cache.http_cache(), stale_while_revalidate_transaction, &response_info); + + EXPECT_EQ(1, cache.network_layer()->transaction_count()); + EXPECT_TRUE(response_info.async_revalidation_requested); + EXPECT_FALSE(response_info.stale_revalidate_timeout.is_null()); + + // Move forward in time such that the stale response is no longer valid. + clock.SetNow(response_info.stale_revalidate_timeout); + clock.Advance(base::TimeDelta::FromSeconds(1)); + + RunTransactionTestWithResponseInfo( + cache.http_cache(), stale_while_revalidate_transaction, &response_info); + + EXPECT_EQ(2, cache.network_layer()->transaction_count()); + EXPECT_FALSE(response_info.async_revalidation_requested); +} + +TEST_F(HttpCacheTest, StaleContentUsedWhenLoadFlagSetAndUsable) { + MockHttpCache cache; + base::SimpleTestClock clock; + cache.http_cache()->SetClockForTesting(&clock); + cache.network_layer()->SetClock(&clock); + clock.Advance(base::TimeDelta::FromSeconds(10)); + + ScopedMockTransaction stale_while_revalidate_transaction( + kSimpleGET_Transaction); + stale_while_revalidate_transaction.load_flags |= + LOAD_SUPPORT_ASYNC_REVALIDATION; + stale_while_revalidate_transaction.response_headers = + "Last-Modified: Sat, 18 Apr 2007 01:10:43 GMT\n" + "Age: 10801\n" + "Cache-Control: max-age=0,stale-while-revalidate=86400\n"; + + // Write to the cache. + RunTransactionTest(cache.http_cache(), stale_while_revalidate_transaction); + + EXPECT_EQ(1, cache.network_layer()->transaction_count()); + + // Send the request again and check that it is not sent to the network again. + HttpResponseInfo response_info; + RunTransactionTestWithResponseInfo( + cache.http_cache(), stale_while_revalidate_transaction, &response_info); + + EXPECT_EQ(1, cache.network_layer()->transaction_count()); + EXPECT_TRUE(response_info.async_revalidation_requested); + EXPECT_FALSE(response_info.stale_revalidate_timeout.is_null()); + base::Time revalidation_timeout = response_info.stale_revalidate_timeout; + clock.Advance(base::TimeDelta::FromSeconds(1)); + EXPECT_TRUE(clock.Now() < revalidation_timeout); + + // Fetch the resource again inside the revalidation timeout window. + RunTransactionTestWithResponseInfo( + cache.http_cache(), stale_while_revalidate_transaction, &response_info); + + EXPECT_EQ(1, cache.network_layer()->transaction_count()); + EXPECT_TRUE(response_info.async_revalidation_requested); + EXPECT_FALSE(response_info.stale_revalidate_timeout.is_null()); + // Expect that the original revalidation timeout hasn't changed. + EXPECT_TRUE(revalidation_timeout == response_info.stale_revalidate_timeout); + + // mask of async revalidation flag. + stale_while_revalidate_transaction.load_flags &= + ~LOAD_SUPPORT_ASYNC_REVALIDATION; + stale_while_revalidate_transaction.status = "HTTP/1.1 304 Not Modified"; + // Write 304 to the cache. + RunTransactionTestWithResponseInfo( + cache.http_cache(), stale_while_revalidate_transaction, &response_info); + + EXPECT_EQ(2, cache.network_layer()->transaction_count()); + EXPECT_FALSE(response_info.async_revalidation_requested); + EXPECT_TRUE(response_info.stale_revalidate_timeout.is_null()); +} + +TEST_F(HttpCacheTest, StaleContentNotUsedWhenUnusable) { + MockHttpCache cache; + + ScopedMockTransaction stale_while_revalidate_transaction( + kSimpleGET_Transaction); + stale_while_revalidate_transaction.load_flags |= + LOAD_SUPPORT_ASYNC_REVALIDATION; + stale_while_revalidate_transaction.response_headers = + "Last-Modified: Sat, 18 Apr 2007 01:10:43 GMT\n" + "Age: 10801\n" + "Cache-Control: max-age=0,stale-while-revalidate=1800\n"; + + // Write to the cache. + RunTransactionTest(cache.http_cache(), stale_while_revalidate_transaction); + + EXPECT_EQ(1, cache.network_layer()->transaction_count()); + + // Send the request again and check that it is sent to the network again. + HttpResponseInfo response_info; + RunTransactionTestWithResponseInfo( + cache.http_cache(), stale_while_revalidate_transaction, &response_info); + + EXPECT_EQ(2, cache.network_layer()->transaction_count()); + EXPECT_FALSE(response_info.async_revalidation_requested); +} + // Tests that we allow multiple simultaneous, non-overlapping transactions to // take place on a sparse entry. TEST_F(HttpCacheTest, RangeGET_MultipleRequests) { diff --git a/net/http/http_response_headers.cc b/net/http/http_response_headers.cc index b9da0223cfe208..7ed98469c11d06 100644 --- a/net/http/http_response_headers.cc +++ b/net/http/http_response_headers.cc @@ -915,14 +915,28 @@ bool HttpResponseHeaders::IsRedirectResponseCode(int response_code) { // Of course, there are other factors that can force a response to always be // validated or re-fetched. // -bool HttpResponseHeaders::RequiresValidation(const Time& request_time, - const Time& response_time, - const Time& current_time) const { +// From RFC 5861 section 3, a stale response may be used while revalidation is +// performed in the background if +// +// freshness_lifetime + stale_while_revalidate > current_age +// +ValidationType HttpResponseHeaders::RequiresValidation( + const Time& request_time, + const Time& response_time, + const Time& current_time) const { FreshnessLifetimes lifetimes = GetFreshnessLifetimes(response_time); - if (lifetimes.freshness.is_zero()) - return true; - return lifetimes.freshness <= - GetCurrentAge(request_time, response_time, current_time); + if (lifetimes.freshness.is_zero() && lifetimes.staleness.is_zero()) + return VALIDATION_SYNCHRONOUS; + + TimeDelta age = GetCurrentAge(request_time, response_time, current_time); + + if (lifetimes.freshness > age) + return VALIDATION_NONE; + + if (lifetimes.freshness + lifetimes.staleness > age) + return VALIDATION_ASYNCHRONOUS; + + return VALIDATION_SYNCHRONOUS; } // From RFC 2616 section 13.2.4: @@ -945,6 +959,9 @@ bool HttpResponseHeaders::RequiresValidation(const Time& request_time, // // freshness_lifetime = (date_value - last_modified_value) * 0.10 // +// If the stale-while-revalidate directive is present, then it is used to set +// the |staleness| time, unless it overridden by another directive. +// HttpResponseHeaders::FreshnessLifetimes HttpResponseHeaders::GetFreshnessLifetimes(const Time& response_time) const { FreshnessLifetimes lifetimes; @@ -957,6 +974,13 @@ HttpResponseHeaders::GetFreshnessLifetimes(const Time& response_time) const { return lifetimes; } + // Cache-Control directive must_revalidate overrides stale-while-revalidate. + bool must_revalidate = HasHeaderValue("cache-control", "must-revalidate"); + + if (must_revalidate || !GetStaleWhileRevalidateValue(&lifetimes.staleness)) { + DCHECK_EQ(TimeDelta(), lifetimes.staleness); + } + // NOTE: "Cache-Control: max-age" overrides Expires, so we only check the // Expires header after checking for max-age in GetFreshnessLifetimes. This // is important since "Expires: " means not fresh, but @@ -1008,7 +1032,7 @@ HttpResponseHeaders::GetFreshnessLifetimes(const Time& response_time) const { // future references ... SHOULD use one of the returned URIs." if ((response_code_ == 200 || response_code_ == 203 || response_code_ == 206) && - !HasHeaderValue("cache-control", "must-revalidate")) { + !must_revalidate) { // TODO(darin): Implement a smarter heuristic. Time last_modified_value; if (GetLastModifiedValue(&last_modified_value)) { @@ -1024,11 +1048,13 @@ HttpResponseHeaders::GetFreshnessLifetimes(const Time& response_time) const { if (response_code_ == 300 || response_code_ == 301 || response_code_ == 308 || response_code_ == 410) { lifetimes.freshness = TimeDelta::Max(); + lifetimes.staleness = TimeDelta(); // It should never be stale. return lifetimes; } // Our heuristic freshness estimate for this resource is 0 seconds, in - // accordance with common browser behaviour. + // accordance with common browser behaviour. However, stale-while-revalidate + // may still apply. DCHECK_EQ(TimeDelta(), lifetimes.freshness); return lifetimes; } @@ -1139,6 +1165,11 @@ bool HttpResponseHeaders::GetExpiresValue(Time* result) const { return GetTimeValuedHeader("Expires", result); } +bool HttpResponseHeaders::GetStaleWhileRevalidateValue( + TimeDelta* result) const { + return GetCacheControlDirective("stale-while-revalidate", result); +} + bool HttpResponseHeaders::GetTimeValuedHeader(const std::string& name, Time* result) const { std::string value; diff --git a/net/http/http_response_headers.h b/net/http/http_response_headers.h index b21435cbdff19f..c33e21c0661e39 100644 --- a/net/http/http_response_headers.h +++ b/net/http/http_response_headers.h @@ -32,6 +32,12 @@ namespace net { class HttpByteRange; class NetLogCaptureMode; +enum ValidationType { + VALIDATION_NONE, // The resource is fresh. + VALIDATION_ASYNCHRONOUS, // The resource requires async revalidation. + VALIDATION_SYNCHRONOUS // The resource requires sync revalidation. +}; + // HttpResponseHeaders: parses and holds HTTP response headers. class NET_EXPORT HttpResponseHeaders : public base::RefCountedThreadSafe { @@ -50,6 +56,9 @@ class NET_EXPORT HttpResponseHeaders struct FreshnessLifetimes { // How long the resource will be fresh for. base::TimeDelta freshness; + // How long after becoming not fresh that the resource will be stale but + // usable (if async revalidation is enabled). + base::TimeDelta staleness; }; static const char kContentRange[]; @@ -203,21 +212,24 @@ class NET_EXPORT HttpResponseHeaders // redirect. static bool IsRedirectResponseCode(int response_code); - // Returns false if the response can be reused without validation. true means + // Returns VALIDATION_NONE if the response can be reused without + // validation. VALIDATION_ASYNCHRONOUS means the response can be re-used, but + // asynchronous revalidation must be performed. VALIDATION_SYNCHRONOUS means // that the result cannot be reused without revalidation. // The result is relative to the current_time parameter, which is // a parameter to support unit testing. The request_time parameter indicates // the time at which the request was made that resulted in this response, // which was received at response_time. - bool RequiresValidation(const base::Time& request_time, - const base::Time& response_time, - const base::Time& current_time) const; + ValidationType RequiresValidation(const base::Time& request_time, + const base::Time& response_time, + const base::Time& current_time) const; // Calculates the amount of time the server claims the response is fresh from // the time the response was generated. See section 13.2.4 of RFC 2616. See // RequiresValidation for a description of the response_time parameter. See // the definition of FreshnessLifetimes above for the meaning of the return - // value. + // value. See RFC 5861 section 3 for the definition of + // stale-while-revalidate. FreshnessLifetimes GetFreshnessLifetimes( const base::Time& response_time) const; @@ -235,6 +247,7 @@ class NET_EXPORT HttpResponseHeaders bool GetDateValue(base::Time* value) const; bool GetLastModifiedValue(base::Time* value) const; bool GetExpiresValue(base::Time* value) const; + bool GetStaleWhileRevalidateValue(base::TimeDelta* value) const; // Extracts the time value of a particular header. This method looks for the // first matching header value and parses its value as a HTTP-date. diff --git a/net/http/http_response_headers_unittest.cc b/net/http/http_response_headers_unittest.cc index 6a4e6085ac4923..c41381f757630e 100644 --- a/net/http/http_response_headers_unittest.cc +++ b/net/http/http_response_headers_unittest.cc @@ -75,6 +75,16 @@ class HttpResponseHeadersCacheControlTest : public HttpResponseHeadersTest { return max_age_value; } + // Get the stale-while-revalidate value. This should only be used in tests + // where a valid max-age parameter is expected to be present. + TimeDelta GetStaleWhileRevalidateValue() { + DCHECK(headers_.get()) << "Call InitializeHeadersWithCacheControl() first"; + TimeDelta stale_while_revalidate_value; + EXPECT_TRUE( + headers()->GetStaleWhileRevalidateValue(&stale_while_revalidate_value)); + return stale_while_revalidate_value; + } + private: scoped_refptr headers_; TimeDelta delta_; @@ -811,7 +821,7 @@ INSTANTIATE_TEST_CASE_P(HttpResponseHeaders, struct RequiresValidationTestData { const char* headers; - bool requires_validation; + ValidationType validation_type; }; class RequiresValidationTest @@ -834,153 +844,173 @@ TEST_P(RequiresValidationTest, RequiresValidation) { HeadersToRaw(&headers); scoped_refptr parsed(new HttpResponseHeaders(headers)); - bool requires_validation = + ValidationType validation_type = parsed->RequiresValidation(request_time, response_time, current_time); - EXPECT_EQ(test.requires_validation, requires_validation); + EXPECT_EQ(test.validation_type, validation_type); } const struct RequiresValidationTestData requires_validation_tests[] = { - // No expiry info: expires immediately. - { "HTTP/1.1 200 OK\n" - "\n", - true - }, - // No expiry info: expires immediately. - { "HTTP/1.1 200 OK\n" - "\n", - true - }, - // Valid for a little while. - { "HTTP/1.1 200 OK\n" - "cache-control: max-age=10000\n" - "\n", - false - }, - // Expires in the future. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "expires: Wed, 28 Nov 2007 01:00:00 GMT\n" - "\n", - false - }, - // Already expired. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "expires: Wed, 28 Nov 2007 00:00:00 GMT\n" - "\n", - true - }, - // Max-age trumps expires. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "expires: Wed, 28 Nov 2007 00:00:00 GMT\n" - "cache-control: max-age=10000\n" - "\n", - false - }, - // Last-modified heuristic: modified a while ago. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" - "\n", - false - }, - { "HTTP/1.1 203 Non-Authoritative Information\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" - "\n", - false - }, - { "HTTP/1.1 206 Partial Content\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" - "\n", - false - }, - // Last-modified heuristic: modified recently. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" - "\n", - true - }, - { "HTTP/1.1 203 Non-Authoritative Information\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" - "\n", - true - }, - { "HTTP/1.1 206 Partial Content\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" - "\n", - true - }, - // Cached permanent redirect. - { "HTTP/1.1 301 Moved Permanently\n" - "\n", - false - }, - // Another cached permanent redirect. - { "HTTP/1.1 308 Permanent Redirect\n" - "\n", - false - }, - // Cached redirect: not reusable even though by default it would be. - { "HTTP/1.1 300 Multiple Choices\n" - "Cache-Control: no-cache\n" - "\n", - true - }, - // Cached forever by default. - { "HTTP/1.1 410 Gone\n" - "\n", - false - }, - // Cached temporary redirect: not reusable. - { "HTTP/1.1 302 Found\n" - "\n", - true - }, - // Cached temporary redirect: reusable. - { "HTTP/1.1 302 Found\n" - "cache-control: max-age=10000\n" - "\n", - false - }, - // Cache-control: max-age=N overrides expires: date in the past. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "expires: Wed, 28 Nov 2007 00:20:11 GMT\n" - "cache-control: max-age=10000\n" - "\n", - false - }, - // Cache-control: no-store overrides expires: in the future. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "expires: Wed, 29 Nov 2007 00:40:11 GMT\n" - "cache-control: no-store,private,no-cache=\"foo\"\n" - "\n", - true - }, - // Pragma: no-cache overrides last-modified heuristic. - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" - "pragma: no-cache\n" - "\n", - true - }, - // max-age has expired, needs synchronous revalidation - { "HTTP/1.1 200 OK\n" - "date: Wed, 28 Nov 2007 00:40:11 GMT\n" - "cache-control: max-age=300\n" - "\n", - true - }, + // No expiry info: expires immediately. + {"HTTP/1.1 200 OK\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // No expiry info: expires immediately. + {"HTTP/1.1 200 OK\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // Valid for a little while. + {"HTTP/1.1 200 OK\n" + "cache-control: max-age=10000\n" + "\n", + VALIDATION_NONE}, + // Expires in the future. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "expires: Wed, 28 Nov 2007 01:00:00 GMT\n" + "\n", + VALIDATION_NONE}, + // Already expired. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "expires: Wed, 28 Nov 2007 00:00:00 GMT\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // Max-age trumps expires. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "expires: Wed, 28 Nov 2007 00:00:00 GMT\n" + "cache-control: max-age=10000\n" + "\n", + VALIDATION_NONE}, + // Last-modified heuristic: modified a while ago. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" + "\n", + VALIDATION_NONE}, + {"HTTP/1.1 203 Non-Authoritative Information\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" + "\n", + VALIDATION_NONE}, + {"HTTP/1.1 206 Partial Content\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" + "\n", + VALIDATION_NONE}, + // Last-modified heuristic: modified recently. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" + "\n", + VALIDATION_SYNCHRONOUS}, + {"HTTP/1.1 203 Non-Authoritative Information\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" + "\n", + VALIDATION_SYNCHRONOUS}, + {"HTTP/1.1 206 Partial Content\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // Cached permanent redirect. + {"HTTP/1.1 301 Moved Permanently\n" + "\n", + VALIDATION_NONE}, + // Another cached permanent redirect. + {"HTTP/1.1 308 Permanent Redirect\n" + "\n", + VALIDATION_NONE}, + // Cached redirect: not reusable even though by default it would be. + {"HTTP/1.1 300 Multiple Choices\n" + "Cache-Control: no-cache\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // Cached forever by default. + {"HTTP/1.1 410 Gone\n" + "\n", + VALIDATION_NONE}, + // Cached temporary redirect: not reusable. + {"HTTP/1.1 302 Found\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // Cached temporary redirect: reusable. + {"HTTP/1.1 302 Found\n" + "cache-control: max-age=10000\n" + "\n", + VALIDATION_NONE}, + // Cache-control: max-age=N overrides expires: date in the past. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "expires: Wed, 28 Nov 2007 00:20:11 GMT\n" + "cache-control: max-age=10000\n" + "\n", + VALIDATION_NONE}, + // Cache-control: no-store overrides expires: in the future. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "expires: Wed, 29 Nov 2007 00:40:11 GMT\n" + "cache-control: no-store,private,no-cache=\"foo\"\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // Pragma: no-cache overrides last-modified heuristic. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" + "pragma: no-cache\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // max-age has expired, needs synchronous revalidation + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "cache-control: max-age=300\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // max-age has expired, stale-while-revalidate has not, eligible for + // asynchronous revalidation + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "cache-control: max-age=300, stale-while-revalidate=3600\n" + "\n", + VALIDATION_ASYNCHRONOUS}, + // max-age and stale-while-revalidate have expired, needs synchronous + // revalidation + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "cache-control: max-age=300, stale-while-revalidate=5\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // max-age is 0, stale-while-revalidate is large enough to permit + // asynchronous revalidation + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "cache-control: max-age=0, stale-while-revalidate=360\n" + "\n", + VALIDATION_ASYNCHRONOUS}, + // stale-while-revalidate must not override no-cache or similar directives. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "cache-control: no-cache, stale-while-revalidate=360\n" + "\n", + VALIDATION_SYNCHRONOUS}, + // max-age has not expired, so no revalidation is needed. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "cache-control: max-age=3600, stale-while-revalidate=3600\n" + "\n", + VALIDATION_NONE}, + // must-revalidate overrides stale-while-revalidate, so synchronous + // validation + // is needed. + {"HTTP/1.1 200 OK\n" + "date: Wed, 28 Nov 2007 00:40:11 GMT\n" + "cache-control: must-revalidate, max-age=300, " + "stale-while-revalidate=3600\n" + "\n", + VALIDATION_SYNCHRONOUS}, - // TODO(darin): Add many many more tests here. + // TODO(darin): Add many many more tests here. }; INSTANTIATE_TEST_CASE_P(HttpResponseHeaders, @@ -2128,6 +2158,36 @@ INSTANTIATE_TEST_CASE_P(HttpResponseHeadersCacheControl, MaxAgeEdgeCasesTest, testing::ValuesIn(max_age_tests)); +TEST_F(HttpResponseHeadersCacheControlTest, + AbsentStaleWhileRevalidateReturnsFalse) { + InitializeHeadersWithCacheControl("max-age=3600"); + EXPECT_FALSE(headers()->GetStaleWhileRevalidateValue(TimeDeltaPointer())); +} + +TEST_F(HttpResponseHeadersCacheControlTest, + StaleWhileRevalidateWithoutValueRejected) { + InitializeHeadersWithCacheControl("max-age=3600,stale-while-revalidate="); + EXPECT_FALSE(headers()->GetStaleWhileRevalidateValue(TimeDeltaPointer())); +} + +TEST_F(HttpResponseHeadersCacheControlTest, + StaleWhileRevalidateWithInvalidValueTreatedAsZero) { + InitializeHeadersWithCacheControl("max-age=3600,stale-while-revalidate=true"); + EXPECT_EQ(TimeDelta(), GetStaleWhileRevalidateValue()); +} + +TEST_F(HttpResponseHeadersCacheControlTest, StaleWhileRevalidateValueReturned) { + InitializeHeadersWithCacheControl("max-age=3600,stale-while-revalidate=7200"); + EXPECT_EQ(TimeDelta::FromSeconds(7200), GetStaleWhileRevalidateValue()); +} + +TEST_F(HttpResponseHeadersCacheControlTest, + FirstStaleWhileRevalidateValueUsed) { + InitializeHeadersWithCacheControl( + "stale-while-revalidate=1,stale-while-revalidate=7200"); + EXPECT_EQ(TimeDelta::FromSeconds(1), GetStaleWhileRevalidateValue()); +} + struct GetCurrentAgeTestData { const char* headers; const char* request_time; diff --git a/net/http/http_response_info.cc b/net/http/http_response_info.cc index 1ec7bf720f4a13..63b3185416c737 100644 --- a/net/http/http_response_info.cc +++ b/net/http/http_response_info.cc @@ -104,6 +104,9 @@ enum { // trust anchor. RESPONSE_INFO_PKP_BYPASSED = 1 << 23, + // This bit is set if stale_revalidate_time is stored. + RESPONSE_INFO_HAS_STALENESS = 1 << 24, + // TODO(darin): Add other bits to indicate alternate request methods. // For now, we don't support storing those. }; @@ -118,6 +121,7 @@ HttpResponseInfo::HttpResponseInfo() was_fetched_via_proxy(false), did_use_http_auth(false), unused_since_prefetch(false), + async_revalidation_requested(false), connection_info(CONNECTION_INFO_UNKNOWN) {} HttpResponseInfo::HttpResponseInfo(const HttpResponseInfo& rhs) = default; @@ -253,6 +257,14 @@ bool HttpResponseInfo::InitFromPickle(const base::Pickle& pickle, ssl_info.key_exchange_group = key_exchange_group; } + // Read staleness time. + if (flags & RESPONSE_INFO_HAS_STALENESS) { + if (!iter.ReadInt64(&time_val)) + return false; + stale_revalidate_timeout = + base::Time() + base::TimeDelta::FromMicroseconds(time_val); + } + was_fetched_via_spdy = (flags & RESPONSE_INFO_WAS_SPDY) != 0; was_alpn_negotiated = (flags & RESPONSE_INFO_WAS_ALPN) != 0; @@ -304,6 +316,8 @@ void HttpResponseInfo::Persist(base::Pickle* pickle, flags |= RESPONSE_INFO_UNUSED_SINCE_PREFETCH; if (ssl_info.pkp_bypassed) flags |= RESPONSE_INFO_PKP_BYPASSED; + if (!stale_revalidate_timeout.is_null()) + flags |= RESPONSE_INFO_HAS_STALENESS; pickle->WriteInt(flags); pickle->WriteInt64(request_time.ToInternalValue()); @@ -346,6 +360,11 @@ void HttpResponseInfo::Persist(base::Pickle* pickle, if (ssl_info.is_valid() && ssl_info.key_exchange_group != 0) pickle->WriteInt(ssl_info.key_exchange_group); + + if (flags & RESPONSE_INFO_HAS_STALENESS) { + pickle->WriteInt64( + (stale_revalidate_timeout - base::Time()).InMicroseconds()); + } } bool HttpResponseInfo::DidUseQuic() const { diff --git a/net/http/http_response_info.h b/net/http/http_response_info.h index 23cef312cac04c..94642e37793f96 100644 --- a/net/http/http_response_info.h +++ b/net/http/http_response_info.h @@ -150,6 +150,16 @@ class NET_EXPORT HttpResponseInfo { // used since. bool unused_since_prefetch; + // True if this resource is stale and needs async revalidation. + // This value is not persisted by Persist(); it is only ever set when the + // response is retrieved from the cache. + bool async_revalidation_requested; + + // stale-while-revalidate, if any, will be honored until time given by + // |stale_revalidate_timeout|. This value is latched the first time + // stale-while-revalidate is used until the resource is revalidated. + base::Time stale_revalidate_timeout; + // Remote address of the socket which fetched this resource. // // NOTE: If the response was served from the cache (was_cached is true), diff --git a/net/http/http_response_info_unittest.cc b/net/http/http_response_info_unittest.cc index d880cffbd09a79..f722c50f8b2574 100644 --- a/net/http/http_response_info_unittest.cc +++ b/net/http/http_response_info_unittest.cc @@ -72,6 +72,56 @@ TEST_F(HttpResponseInfoTest, PKPBypassPersistFalse) { EXPECT_FALSE(restored_response_info.ssl_info.pkp_bypassed); } +TEST_F(HttpResponseInfoTest, AsyncRevalidationRequestedDefault) { + EXPECT_FALSE(response_info_.async_revalidation_requested); +} + +TEST_F(HttpResponseInfoTest, AsyncRevalidationRequestedCopy) { + response_info_.async_revalidation_requested = true; + net::HttpResponseInfo response_info_clone(response_info_); + EXPECT_TRUE(response_info_clone.async_revalidation_requested); +} + +TEST_F(HttpResponseInfoTest, AsyncRevalidationRequestedAssign) { + response_info_.async_revalidation_requested = true; + net::HttpResponseInfo response_info_clone; + response_info_clone = response_info_; + EXPECT_TRUE(response_info_clone.async_revalidation_requested); +} + +TEST_F(HttpResponseInfoTest, AsyncRevalidationRequestedNotPersisted) { + response_info_.async_revalidation_requested = true; + net::HttpResponseInfo restored_response_info; + PickleAndRestore(response_info_, &restored_response_info); + EXPECT_FALSE(restored_response_info.async_revalidation_requested); +} + +TEST_F(HttpResponseInfoTest, StaleRevalidationTimeoutDefault) { + EXPECT_TRUE(response_info_.stale_revalidate_timeout.is_null()); +} + +TEST_F(HttpResponseInfoTest, StaleRevalidationTimeoutCopy) { + base::Time test_time = base::Time::FromDoubleT(1000); + response_info_.stale_revalidate_timeout = test_time; + HttpResponseInfo response_info_clone(response_info_); + EXPECT_EQ(test_time, response_info_clone.stale_revalidate_timeout); +} + +TEST_F(HttpResponseInfoTest, StaleRevalidationTimeoutRestoreValue) { + base::Time test_time = base::Time::FromDoubleT(1000); + response_info_.stale_revalidate_timeout = test_time; + HttpResponseInfo restored_response_info; + PickleAndRestore(response_info_, &restored_response_info); + EXPECT_EQ(test_time, restored_response_info.stale_revalidate_timeout); +} + +TEST_F(HttpResponseInfoTest, StaleRevalidationTimeoutRestoreNoValue) { + EXPECT_TRUE(response_info_.stale_revalidate_timeout.is_null()); + HttpResponseInfo restored_response_info; + PickleAndRestore(response_info_, &restored_response_info); + EXPECT_TRUE(restored_response_info.stale_revalidate_timeout.is_null()); +} + // Test that key_exchange_group is preserved for ECDHE ciphers. TEST_F(HttpResponseInfoTest, KeyExchangeGroupECDHE) { response_info_.ssl_info.cert =