From c5d26252a7b01caf919b9842eed347725a6795eb Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Tue, 26 May 2020 14:12:18 +0000 Subject: [PATCH] Apply stale-while-revalidate also for responses without a validator Due to its name, the stale-while-revalidate Cache-Control extension might suggest that it only applies to revalidation requests. However, looking at https://tools.ietf.org/html/rfc5861#section-3... > The stale-while-revalidate Cache-Control Extension > When present in an HTTP response, the stale-while-revalidate > Cache-Control extension indicates that caches MAY serve the > response in which it appears after it becomes stale, up to the > indicated number of seconds. > stale-while-revalidate = "stale-while-revalidate" "=" delta-seconds > If a cached response is served stale due to the presence of this > extension, the cache SHOULD attempt to revalidate it while still > serving stale responses (i.e., without blocking). There is no reason why a cache should not also return a stale response while a complete re-fetch happens in the background (just what would happen if validation fails). This makes a difference if, for example, a resource has `Cache-Control: public, max-age=30, stale-while-revalidate=30` set and no additional Last-Modified or ETag headers. Co-Authored-By: Philip Hofstetter getResponse()->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_HIT) ); - } elseif ($staleResponse - || ($maxStaleCache !== null && $cacheEntry->getStaleAge() <= $maxStaleCache) + } elseif ($cacheEntry->staleWhileValidate() + && (($maxStaleCache === null) || ($staleResponse || $cacheEntry->getStaleAge() <= $maxStaleCache)) ) { - // Staled cache! + /* + * The cached response indicated that it may be served stale while background revalidation (or fetch) + * occurs, and the client did not limit maximum staleness. (https://tools.ietf.org/html/rfc5861#section-3) + * + * Return the cached, stale response; initiate deferred revalidation/re-fetch. + */ + static::addReValidationRequest( + static::getRequestWithReValidationHeader($request, $cacheEntry), + $this->cacheStorage, + $cacheEntry + ); + return new FulfilledPromise( - $cacheEntry->getResponse()->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_HIT) + $cacheEntry->getResponse() + ->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE) ); - } elseif ($cacheEntry->hasValidationInformation() && !$onlyFromCache) { + } elseif (($cacheEntry->staleWhileValidate() && $cacheEntry->getStaleAge() <= $maxStaleCache) || ($cacheEntry->hasValidationInformation() && !$onlyFromCache)) { // Re-validation header $request = static::getRequestWithReValidationHeader($request, $cacheEntry); - - if ($cacheEntry->staleWhileValidate()) { - static::addReValidationRequest($request, $this->cacheStorage, $cacheEntry); - - return new FulfilledPromise( - $cacheEntry->getResponse() - ->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_STALE) - ); - } } } else { $cacheEntry = null; diff --git a/tests/RequestCacheControlTest.php b/tests/RequestCacheControlTest.php index 5dbdcc9..247e94e 100644 --- a/tests/RequestCacheControlTest.php +++ b/tests/RequestCacheControlTest.php @@ -42,6 +42,11 @@ protected function setUp(): void (new Response()) ->withAddedHeader('Cache-Control', 'max-age=3') ); + case '/1s-stale-while-revalidate': + return new FulfilledPromise( + (new Response()) + ->withAddedHeader('Cache-Control', 'max-age=1, stale-while-revalidate=3') + ); } throw new \InvalidArgumentException(); @@ -162,6 +167,41 @@ public function testMaxStaleHeader() CacheMiddleware::HEADER_CACHE_MISS, $response->getHeaderLine(CacheMiddleware::HEADER_CACHE_INFO) ); + + $response = $this->client->get('http://test.com/1s-stale-while-revalidate', [ + 'headers' => [ + 'Cache-Control' => 'max-stale', + ] + ]); + $this->assertEquals( + CacheMiddleware::HEADER_CACHE_MISS, + $response->getHeaderLine(CacheMiddleware::HEADER_CACHE_INFO) + ); + + sleep(2); + + $response = $this->client->get('http://test.com/1s-stale-while-revalidate', [ + 'headers' => [ + 'Cache-Control' => 'max-stale=1', + ] + ]); + $this->assertEquals( + CacheMiddleware::HEADER_CACHE_STALE, + $response->getHeaderLine(CacheMiddleware::HEADER_CACHE_INFO) + ); + + sleep(1); + + $response = $this->client->get('http://test.com/1s-stale-while-revalidate', [ + 'headers' => [ + 'Cache-Control' => 'max-stale=1', + ] + ]); + $this->assertEquals( + CacheMiddleware::HEADER_CACHE_MISS, + $response->getHeaderLine(CacheMiddleware::HEADER_CACHE_INFO) + ); + } public function testMinFreshHeader() diff --git a/tests/ResponseCacheControlTest.php b/tests/ResponseCacheControlTest.php index 79e3353..fe62810 100644 --- a/tests/ResponseCacheControlTest.php +++ b/tests/ResponseCacheControlTest.php @@ -30,7 +30,7 @@ protected function setUp(): void case '/2s-complex': return new FulfilledPromise( (new Response()) - ->withAddedHeader('Cache-Control', 'invalid-token="yes", max-age=2, stale-while-revalidate=60') + ->withAddedHeader('Cache-Control', 'invalid-token="yes", max-age=2, stale-while-revalidate=3') ); case '/no-store': return new FulfilledPromise( @@ -91,6 +91,11 @@ public function testMaxAgeComplexHeader() sleep(3); + $response = $this->client->get('http://test.com/2s-complex'); + $this->assertEquals(CacheMiddleware::HEADER_CACHE_STALE, $response->getHeaderLine(CacheMiddleware::HEADER_CACHE_INFO)); + + sleep(5); + $response = $this->client->get('http://test.com/2s-complex'); $this->assertEquals(CacheMiddleware::HEADER_CACHE_MISS, $response->getHeaderLine(CacheMiddleware::HEADER_CACHE_INFO)); }