4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\HttpKernel\Tests\HttpCache;
14 use Symfony\Component\HttpFoundation\Request;
15 use Symfony\Component\HttpFoundation\Response;
16 use Symfony\Component\HttpKernel\HttpCache\Esi;
17 use Symfony\Component\HttpKernel\HttpCache\HttpCache;
18 use Symfony\Component\HttpKernel\HttpCache\Store;
19 use Symfony\Component\HttpKernel\HttpKernelInterface;
22 * @group time-sensitive
24 class HttpCacheTest extends HttpCacheTestCase
26 public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
28 $storeMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpCache\\StoreInterface')
29 ->disableOriginalConstructor()
32 // does not implement TerminableInterface
33 $kernel = new TestKernel();
34 $httpCache = new HttpCache($kernel, $storeMock);
35 $httpCache->terminate(Request::create('/'), new Response());
37 $this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface');
39 // implements TerminableInterface
40 $kernelMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel')
41 ->disableOriginalConstructor()
42 ->setMethods(array('terminate', 'registerBundles', 'registerContainerConfiguration'))
45 $kernelMock->expects($this->once())
46 ->method('terminate');
48 $kernel = new HttpCache($kernelMock, $storeMock);
49 $kernel->terminate(Request::create('/'), new Response());
52 public function testPassesOnNonGetHeadRequests()
54 $this->setNextResponse(200);
55 $this->request('POST', '/');
56 $this->assertHttpKernelIsCalled();
57 $this->assertResponseOk();
58 $this->assertTraceContains('pass');
59 $this->assertFalse($this->response->headers->has('Age'));
62 public function testInvalidatesOnPostPutDeleteRequests()
64 foreach (array('post', 'put', 'delete') as $method) {
65 $this->setNextResponse(200);
66 $this->request($method, '/');
68 $this->assertHttpKernelIsCalled();
69 $this->assertResponseOk();
70 $this->assertTraceContains('invalidate');
71 $this->assertTraceContains('pass');
75 public function testDoesNotCacheWithAuthorizationRequestHeaderAndNonPublicResponse()
77 $this->setNextResponse(200, array('ETag' => '"Foo"'));
78 $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
80 $this->assertHttpKernelIsCalled();
81 $this->assertResponseOk();
82 $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
84 $this->assertTraceContains('miss');
85 $this->assertTraceNotContains('store');
86 $this->assertFalse($this->response->headers->has('Age'));
89 public function testDoesCacheWithAuthorizationRequestHeaderAndPublicResponse()
91 $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"Foo"'));
92 $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
94 $this->assertHttpKernelIsCalled();
95 $this->assertResponseOk();
96 $this->assertTraceContains('miss');
97 $this->assertTraceContains('store');
98 $this->assertTrue($this->response->headers->has('Age'));
99 $this->assertEquals('public', $this->response->headers->get('Cache-Control'));
102 public function testDoesNotCacheWithCookieHeaderAndNonPublicResponse()
104 $this->setNextResponse(200, array('ETag' => '"Foo"'));
105 $this->request('GET', '/', array(), array('foo' => 'bar'));
107 $this->assertHttpKernelIsCalled();
108 $this->assertResponseOk();
109 $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
110 $this->assertTraceContains('miss');
111 $this->assertTraceNotContains('store');
112 $this->assertFalse($this->response->headers->has('Age'));
115 public function testDoesNotCacheRequestsWithACookieHeader()
117 $this->setNextResponse(200);
118 $this->request('GET', '/', array(), array('foo' => 'bar'));
120 $this->assertHttpKernelIsCalled();
121 $this->assertResponseOk();
122 $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
123 $this->assertTraceContains('miss');
124 $this->assertTraceNotContains('store');
125 $this->assertFalse($this->response->headers->has('Age'));
128 public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified()
130 $time = \DateTime::createFromFormat('U', time());
132 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822), 'Content-Type' => 'text/plain'), 'Hello World');
133 $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
135 $this->assertHttpKernelIsCalled();
136 $this->assertEquals(304, $this->response->getStatusCode());
137 $this->assertEquals('', $this->response->headers->get('Content-Type'));
138 $this->assertEmpty($this->response->getContent());
139 $this->assertTraceContains('miss');
140 $this->assertTraceContains('store');
143 public function testRespondsWith304WhenIfNoneMatchMatchesETag()
145 $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '12345', 'Content-Type' => 'text/plain'), 'Hello World');
146 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345'));
148 $this->assertHttpKernelIsCalled();
149 $this->assertEquals(304, $this->response->getStatusCode());
150 $this->assertEquals('', $this->response->headers->get('Content-Type'));
151 $this->assertTrue($this->response->headers->has('ETag'));
152 $this->assertEmpty($this->response->getContent());
153 $this->assertTraceContains('miss');
154 $this->assertTraceContains('store');
157 public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch()
159 $time = \DateTime::createFromFormat('U', time());
161 $this->setNextResponse(200, array(), '', function ($request, $response) use ($time) {
162 $response->setStatusCode(200);
163 $response->headers->set('ETag', '12345');
164 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
165 $response->headers->set('Content-Type', 'text/plain');
166 $response->setContent('Hello World');
170 $t = \DateTime::createFromFormat('U', time() - 3600);
171 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(DATE_RFC2822)));
172 $this->assertHttpKernelIsCalled();
173 $this->assertEquals(200, $this->response->getStatusCode());
175 // only Last-Modified matches
176 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
177 $this->assertHttpKernelIsCalled();
178 $this->assertEquals(200, $this->response->getStatusCode());
181 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
182 $this->assertHttpKernelIsCalled();
183 $this->assertEquals(304, $this->response->getStatusCode());
186 public function testIncrementsMaxAgeWhenNoDateIsSpecifiedEventWhenUsingETag()
188 $this->setNextResponse(
192 'Cache-Control' => 'public, s-maxage=60',
196 $this->request('GET', '/');
197 $this->assertHttpKernelIsCalled();
198 $this->assertEquals(200, $this->response->getStatusCode());
199 $this->assertTraceContains('miss');
200 $this->assertTraceContains('store');
204 $this->request('GET', '/');
205 $this->assertHttpKernelIsNotCalled();
206 $this->assertEquals(200, $this->response->getStatusCode());
207 $this->assertTraceContains('fresh');
208 $this->assertEquals(2, $this->response->headers->get('Age'));
211 public function testValidatesPrivateResponsesCachedOnTheClient()
213 $this->setNextResponse(200, array(), '', function ($request, $response) {
214 $etags = preg_split('/\s*,\s*/', $request->headers->get('IF_NONE_MATCH'));
215 if ($request->cookies->has('authenticated')) {
216 $response->headers->set('Cache-Control', 'private, no-store');
217 $response->setETag('"private tag"');
218 if (\in_array('"private tag"', $etags)) {
219 $response->setStatusCode(304);
221 $response->setStatusCode(200);
222 $response->headers->set('Content-Type', 'text/plain');
223 $response->setContent('private data');
226 $response->headers->set('Cache-Control', 'public');
227 $response->setETag('"public tag"');
228 if (\in_array('"public tag"', $etags)) {
229 $response->setStatusCode(304);
231 $response->setStatusCode(200);
232 $response->headers->set('Content-Type', 'text/plain');
233 $response->setContent('public data');
238 $this->request('GET', '/');
239 $this->assertHttpKernelIsCalled();
240 $this->assertEquals(200, $this->response->getStatusCode());
241 $this->assertEquals('"public tag"', $this->response->headers->get('ETag'));
242 $this->assertEquals('public data', $this->response->getContent());
243 $this->assertTraceContains('miss');
244 $this->assertTraceContains('store');
246 $this->request('GET', '/', array(), array('authenticated' => ''));
247 $this->assertHttpKernelIsCalled();
248 $this->assertEquals(200, $this->response->getStatusCode());
249 $this->assertEquals('"private tag"', $this->response->headers->get('ETag'));
250 $this->assertEquals('private data', $this->response->getContent());
251 $this->assertTraceContains('stale');
252 $this->assertTraceContains('invalid');
253 $this->assertTraceNotContains('store');
256 public function testStoresResponsesWhenNoCacheRequestDirectivePresent()
258 $time = \DateTime::createFromFormat('U', time() + 5);
260 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
261 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
263 $this->assertHttpKernelIsCalled();
264 $this->assertTraceContains('store');
265 $this->assertTrue($this->response->headers->has('Age'));
268 public function testReloadsResponsesWhenCacheHitsButNoCacheRequestDirectivePresentWhenAllowReloadIsSetTrue()
272 $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
274 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
277 $this->request('GET', '/');
278 $this->assertEquals(200, $this->response->getStatusCode());
279 $this->assertEquals('Hello World', $this->response->getContent());
280 $this->assertTraceContains('store');
282 $this->request('GET', '/');
283 $this->assertEquals(200, $this->response->getStatusCode());
284 $this->assertEquals('Hello World', $this->response->getContent());
285 $this->assertTraceContains('fresh');
287 $this->cacheConfig['allow_reload'] = true;
288 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
289 $this->assertEquals(200, $this->response->getStatusCode());
290 $this->assertEquals('Goodbye World', $this->response->getContent());
291 $this->assertTraceContains('reload');
292 $this->assertTraceContains('store');
295 public function testDoesNotReloadResponsesWhenAllowReloadIsSetFalseDefault()
299 $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
301 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
304 $this->request('GET', '/');
305 $this->assertEquals(200, $this->response->getStatusCode());
306 $this->assertEquals('Hello World', $this->response->getContent());
307 $this->assertTraceContains('store');
309 $this->request('GET', '/');
310 $this->assertEquals(200, $this->response->getStatusCode());
311 $this->assertEquals('Hello World', $this->response->getContent());
312 $this->assertTraceContains('fresh');
314 $this->cacheConfig['allow_reload'] = false;
315 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
316 $this->assertEquals(200, $this->response->getStatusCode());
317 $this->assertEquals('Hello World', $this->response->getContent());
318 $this->assertTraceNotContains('reload');
320 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
321 $this->assertEquals(200, $this->response->getStatusCode());
322 $this->assertEquals('Hello World', $this->response->getContent());
323 $this->assertTraceNotContains('reload');
326 public function testRevalidatesFreshCacheEntryWhenMaxAgeRequestDirectiveIsExceededWhenAllowRevalidateOptionIsSetTrue()
330 $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
332 $response->headers->set('Cache-Control', 'public, max-age=10000');
333 $response->setETag($count);
334 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
337 $this->request('GET', '/');
338 $this->assertEquals(200, $this->response->getStatusCode());
339 $this->assertEquals('Hello World', $this->response->getContent());
340 $this->assertTraceContains('store');
342 $this->request('GET', '/');
343 $this->assertEquals(200, $this->response->getStatusCode());
344 $this->assertEquals('Hello World', $this->response->getContent());
345 $this->assertTraceContains('fresh');
347 $this->cacheConfig['allow_revalidate'] = true;
348 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
349 $this->assertEquals(200, $this->response->getStatusCode());
350 $this->assertEquals('Goodbye World', $this->response->getContent());
351 $this->assertTraceContains('stale');
352 $this->assertTraceContains('invalid');
353 $this->assertTraceContains('store');
356 public function testDoesNotRevalidateFreshCacheEntryWhenEnableRevalidateOptionIsSetFalseDefault()
360 $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
362 $response->headers->set('Cache-Control', 'public, max-age=10000');
363 $response->setETag($count);
364 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
367 $this->request('GET', '/');
368 $this->assertEquals(200, $this->response->getStatusCode());
369 $this->assertEquals('Hello World', $this->response->getContent());
370 $this->assertTraceContains('store');
372 $this->request('GET', '/');
373 $this->assertEquals(200, $this->response->getStatusCode());
374 $this->assertEquals('Hello World', $this->response->getContent());
375 $this->assertTraceContains('fresh');
377 $this->cacheConfig['allow_revalidate'] = false;
378 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
379 $this->assertEquals(200, $this->response->getStatusCode());
380 $this->assertEquals('Hello World', $this->response->getContent());
381 $this->assertTraceNotContains('stale');
382 $this->assertTraceNotContains('invalid');
383 $this->assertTraceContains('fresh');
385 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
386 $this->assertEquals(200, $this->response->getStatusCode());
387 $this->assertEquals('Hello World', $this->response->getContent());
388 $this->assertTraceNotContains('stale');
389 $this->assertTraceNotContains('invalid');
390 $this->assertTraceContains('fresh');
393 public function testFetchesResponseFromBackendWhenCacheMisses()
395 $time = \DateTime::createFromFormat('U', time() + 5);
396 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
398 $this->request('GET', '/');
399 $this->assertEquals(200, $this->response->getStatusCode());
400 $this->assertTraceContains('miss');
401 $this->assertTrue($this->response->headers->has('Age'));
404 public function testDoesNotCacheSomeStatusCodeResponses()
406 foreach (array_merge(range(201, 202), range(204, 206), range(303, 305), range(400, 403), range(405, 409), range(411, 417), range(500, 505)) as $code) {
407 $time = \DateTime::createFromFormat('U', time() + 5);
408 $this->setNextResponse($code, array('Expires' => $time->format(DATE_RFC2822)));
410 $this->request('GET', '/');
411 $this->assertEquals($code, $this->response->getStatusCode());
412 $this->assertTraceNotContains('store');
413 $this->assertFalse($this->response->headers->has('Age'));
417 public function testDoesNotCacheResponsesWithExplicitNoStoreDirective()
419 $time = \DateTime::createFromFormat('U', time() + 5);
420 $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'no-store'));
422 $this->request('GET', '/');
423 $this->assertTraceNotContains('store');
424 $this->assertFalse($this->response->headers->has('Age'));
427 public function testDoesNotCacheResponsesWithoutFreshnessInformationOrAValidator()
429 $this->setNextResponse();
431 $this->request('GET', '/');
432 $this->assertEquals(200, $this->response->getStatusCode());
433 $this->assertTraceNotContains('store');
436 public function testCachesResponsesWithExplicitNoCacheDirective()
438 $time = \DateTime::createFromFormat('U', time() + 5);
439 $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, no-cache'));
441 $this->request('GET', '/');
442 $this->assertTraceContains('store');
443 $this->assertTrue($this->response->headers->has('Age'));
446 public function testCachesResponsesWithAnExpirationHeader()
448 $time = \DateTime::createFromFormat('U', time() + 5);
449 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
451 $this->request('GET', '/');
452 $this->assertEquals(200, $this->response->getStatusCode());
453 $this->assertEquals('Hello World', $this->response->getContent());
454 $this->assertNotNull($this->response->headers->get('Date'));
455 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
456 $this->assertTraceContains('miss');
457 $this->assertTraceContains('store');
459 $values = $this->getMetaStorageValues();
460 $this->assertCount(1, $values);
463 public function testCachesResponsesWithAMaxAgeDirective()
465 $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=5'));
467 $this->request('GET', '/');
468 $this->assertEquals(200, $this->response->getStatusCode());
469 $this->assertEquals('Hello World', $this->response->getContent());
470 $this->assertNotNull($this->response->headers->get('Date'));
471 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
472 $this->assertTraceContains('miss');
473 $this->assertTraceContains('store');
475 $values = $this->getMetaStorageValues();
476 $this->assertCount(1, $values);
479 public function testCachesResponsesWithASMaxAgeDirective()
481 $this->setNextResponse(200, array('Cache-Control' => 's-maxage=5'));
483 $this->request('GET', '/');
484 $this->assertEquals(200, $this->response->getStatusCode());
485 $this->assertEquals('Hello World', $this->response->getContent());
486 $this->assertNotNull($this->response->headers->get('Date'));
487 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
488 $this->assertTraceContains('miss');
489 $this->assertTraceContains('store');
491 $values = $this->getMetaStorageValues();
492 $this->assertCount(1, $values);
495 public function testCachesResponsesWithALastModifiedValidatorButNoFreshnessInformation()
497 $time = \DateTime::createFromFormat('U', time());
498 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822)));
500 $this->request('GET', '/');
501 $this->assertEquals(200, $this->response->getStatusCode());
502 $this->assertEquals('Hello World', $this->response->getContent());
503 $this->assertTraceContains('miss');
504 $this->assertTraceContains('store');
507 public function testCachesResponsesWithAnETagValidatorButNoFreshnessInformation()
509 $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"123456"'));
511 $this->request('GET', '/');
512 $this->assertEquals(200, $this->response->getStatusCode());
513 $this->assertEquals('Hello World', $this->response->getContent());
514 $this->assertTraceContains('miss');
515 $this->assertTraceContains('store');
518 public function testHitsCachedResponsesWithExpiresHeader()
520 $time1 = \DateTime::createFromFormat('U', time() - 5);
521 $time2 = \DateTime::createFromFormat('U', time() + 5);
522 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Date' => $time1->format(DATE_RFC2822), 'Expires' => $time2->format(DATE_RFC2822)));
524 $this->request('GET', '/');
525 $this->assertHttpKernelIsCalled();
526 $this->assertEquals(200, $this->response->getStatusCode());
527 $this->assertNotNull($this->response->headers->get('Date'));
528 $this->assertTraceContains('miss');
529 $this->assertTraceContains('store');
530 $this->assertEquals('Hello World', $this->response->getContent());
532 $this->request('GET', '/');
533 $this->assertHttpKernelIsNotCalled();
534 $this->assertEquals(200, $this->response->getStatusCode());
535 $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
536 $this->assertGreaterThan(0, $this->response->headers->get('Age'));
537 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
538 $this->assertTraceContains('fresh');
539 $this->assertTraceNotContains('store');
540 $this->assertEquals('Hello World', $this->response->getContent());
543 public function testHitsCachedResponseWithMaxAgeDirective()
545 $time = \DateTime::createFromFormat('U', time() - 5);
546 $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, max-age=10'));
548 $this->request('GET', '/');
549 $this->assertHttpKernelIsCalled();
550 $this->assertEquals(200, $this->response->getStatusCode());
551 $this->assertNotNull($this->response->headers->get('Date'));
552 $this->assertTraceContains('miss');
553 $this->assertTraceContains('store');
554 $this->assertEquals('Hello World', $this->response->getContent());
556 $this->request('GET', '/');
557 $this->assertHttpKernelIsNotCalled();
558 $this->assertEquals(200, $this->response->getStatusCode());
559 $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
560 $this->assertGreaterThan(0, $this->response->headers->get('Age'));
561 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
562 $this->assertTraceContains('fresh');
563 $this->assertTraceNotContains('store');
564 $this->assertEquals('Hello World', $this->response->getContent());
567 public function testDegradationWhenCacheLocked()
569 if ('\\' === \DIRECTORY_SEPARATOR) {
570 $this->markTestSkipped('Skips on windows to avoid permissions issues.');
573 $this->cacheConfig['stale_while_revalidate'] = 10;
575 // The prescence of Last-Modified makes this cacheable (because Response::isValidateable() then).
576 $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=5', 'Last-Modified' => 'some while ago'), 'Old response');
577 $this->request('GET', '/'); // warm the cache
579 // Now, lock the cache
580 $concurrentRequest = Request::create('/', 'GET');
581 $this->store->lock($concurrentRequest);
584 * After 10s, the cached response has become stale. Yet, we're still within the "stale_while_revalidate"
585 * timeout so we may serve the stale response.
589 $this->request('GET', '/');
590 $this->assertHttpKernelIsNotCalled();
591 $this->assertEquals(200, $this->response->getStatusCode());
592 $this->assertTraceContains('stale-while-revalidate');
593 $this->assertEquals('Old response', $this->response->getContent());
596 * Another 10s later, stale_while_revalidate is over. Resort to serving the old response, but
597 * do so with a "server unavailable" message.
601 $this->request('GET', '/');
602 $this->assertHttpKernelIsNotCalled();
603 $this->assertEquals(503, $this->response->getStatusCode());
604 $this->assertEquals('Old response', $this->response->getContent());
607 public function testHitsCachedResponseWithSMaxAgeDirective()
609 $time = \DateTime::createFromFormat('U', time() - 5);
610 $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 's-maxage=10, max-age=0'));
612 $this->request('GET', '/');
613 $this->assertHttpKernelIsCalled();
614 $this->assertEquals(200, $this->response->getStatusCode());
615 $this->assertNotNull($this->response->headers->get('Date'));
616 $this->assertTraceContains('miss');
617 $this->assertTraceContains('store');
618 $this->assertEquals('Hello World', $this->response->getContent());
620 $this->request('GET', '/');
621 $this->assertHttpKernelIsNotCalled();
622 $this->assertEquals(200, $this->response->getStatusCode());
623 $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
624 $this->assertGreaterThan(0, $this->response->headers->get('Age'));
625 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
626 $this->assertTraceContains('fresh');
627 $this->assertTraceNotContains('store');
628 $this->assertEquals('Hello World', $this->response->getContent());
631 public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation()
633 $this->setNextResponse();
635 $this->cacheConfig['default_ttl'] = 10;
636 $this->request('GET', '/');
637 $this->assertHttpKernelIsCalled();
638 $this->assertTraceContains('miss');
639 $this->assertTraceContains('store');
640 $this->assertEquals('Hello World', $this->response->getContent());
641 $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control'));
643 $this->cacheConfig['default_ttl'] = 10;
644 $this->request('GET', '/');
645 $this->assertHttpKernelIsNotCalled();
646 $this->assertEquals(200, $this->response->getStatusCode());
647 $this->assertTraceContains('fresh');
648 $this->assertTraceNotContains('store');
649 $this->assertEquals('Hello World', $this->response->getContent());
650 $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control'));
653 public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpired()
655 $this->setNextResponse();
657 $this->cacheConfig['default_ttl'] = 2;
658 $this->request('GET', '/');
659 $this->assertHttpKernelIsCalled();
660 $this->assertTraceContains('miss');
661 $this->assertTraceContains('store');
662 $this->assertEquals('Hello World', $this->response->getContent());
663 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
665 $this->request('GET', '/');
666 $this->assertHttpKernelIsNotCalled();
667 $this->assertEquals(200, $this->response->getStatusCode());
668 $this->assertTraceContains('fresh');
669 $this->assertTraceNotContains('store');
670 $this->assertEquals('Hello World', $this->response->getContent());
671 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
674 $values = $this->getMetaStorageValues();
675 $this->assertCount(1, $values);
676 $tmp = unserialize($values[0]);
677 $time = \DateTime::createFromFormat('U', time() - 5);
678 $tmp[0][1]['date'] = $time->format(DATE_RFC2822);
679 $r = new \ReflectionObject($this->store);
680 $m = $r->getMethod('save');
681 $m->setAccessible(true);
682 $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
684 $this->request('GET', '/');
685 $this->assertHttpKernelIsCalled();
686 $this->assertEquals(200, $this->response->getStatusCode());
687 $this->assertTraceContains('stale');
688 $this->assertTraceContains('invalid');
689 $this->assertTraceContains('store');
690 $this->assertEquals('Hello World', $this->response->getContent());
691 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
693 $this->setNextResponse();
695 $this->request('GET', '/');
696 $this->assertHttpKernelIsNotCalled();
697 $this->assertEquals(200, $this->response->getStatusCode());
698 $this->assertTraceContains('fresh');
699 $this->assertTraceNotContains('store');
700 $this->assertEquals('Hello World', $this->response->getContent());
701 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
704 public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpiredWithStatus304()
706 $this->setNextResponse();
708 $this->cacheConfig['default_ttl'] = 2;
709 $this->request('GET', '/');
710 $this->assertHttpKernelIsCalled();
711 $this->assertTraceContains('miss');
712 $this->assertTraceContains('store');
713 $this->assertEquals('Hello World', $this->response->getContent());
714 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
716 $this->request('GET', '/');
717 $this->assertHttpKernelIsNotCalled();
718 $this->assertEquals(200, $this->response->getStatusCode());
719 $this->assertTraceContains('fresh');
720 $this->assertTraceNotContains('store');
721 $this->assertEquals('Hello World', $this->response->getContent());
724 $values = $this->getMetaStorageValues();
725 $this->assertCount(1, $values);
726 $tmp = unserialize($values[0]);
727 $time = \DateTime::createFromFormat('U', time() - 5);
728 $tmp[0][1]['date'] = $time->format(DATE_RFC2822);
729 $r = new \ReflectionObject($this->store);
730 $m = $r->getMethod('save');
731 $m->setAccessible(true);
732 $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
734 $this->request('GET', '/');
735 $this->assertHttpKernelIsCalled();
736 $this->assertEquals(200, $this->response->getStatusCode());
737 $this->assertTraceContains('stale');
738 $this->assertTraceContains('valid');
739 $this->assertTraceContains('store');
740 $this->assertTraceNotContains('miss');
741 $this->assertEquals('Hello World', $this->response->getContent());
742 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
744 $this->request('GET', '/');
745 $this->assertHttpKernelIsNotCalled();
746 $this->assertEquals(200, $this->response->getStatusCode());
747 $this->assertTraceContains('fresh');
748 $this->assertTraceNotContains('store');
749 $this->assertEquals('Hello World', $this->response->getContent());
750 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
753 public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirective()
755 $this->setNextResponse(200, array('Cache-Control' => 'must-revalidate'));
757 $this->cacheConfig['default_ttl'] = 10;
758 $this->request('GET', '/');
759 $this->assertHttpKernelIsCalled();
760 $this->assertEquals(200, $this->response->getStatusCode());
761 $this->assertTraceContains('miss');
762 $this->assertTraceNotContains('store');
763 $this->assertNotRegExp('/s-maxage/', $this->response->headers->get('Cache-Control'));
764 $this->assertEquals('Hello World', $this->response->getContent());
767 public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent()
769 $time = \DateTime::createFromFormat('U', time() + 5);
770 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
772 // build initial request
773 $this->request('GET', '/');
774 $this->assertHttpKernelIsCalled();
775 $this->assertEquals(200, $this->response->getStatusCode());
776 $this->assertNotNull($this->response->headers->get('Date'));
777 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
778 $this->assertNotNull($this->response->headers->get('Age'));
779 $this->assertTraceContains('miss');
780 $this->assertTraceContains('store');
781 $this->assertEquals('Hello World', $this->response->getContent());
783 // go in and play around with the cached metadata directly ...
784 $values = $this->getMetaStorageValues();
785 $this->assertCount(1, $values);
786 $tmp = unserialize($values[0]);
787 $time = \DateTime::createFromFormat('U', time());
788 $tmp[0][1]['expires'] = $time->format(DATE_RFC2822);
789 $r = new \ReflectionObject($this->store);
790 $m = $r->getMethod('save');
791 $m->setAccessible(true);
792 $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
794 // build subsequent request; should be found but miss due to freshness
795 $this->request('GET', '/');
796 $this->assertHttpKernelIsCalled();
797 $this->assertEquals(200, $this->response->getStatusCode());
798 $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
799 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
800 $this->assertTraceContains('stale');
801 $this->assertTraceNotContains('fresh');
802 $this->assertTraceNotContains('miss');
803 $this->assertTraceContains('store');
804 $this->assertEquals('Hello World', $this->response->getContent());
807 public function testValidatesCachedResponsesWithLastModifiedAndNoFreshnessInformation()
809 $time = \DateTime::createFromFormat('U', time());
810 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time) {
811 $response->headers->set('Cache-Control', 'public');
812 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
813 if ($time->format(DATE_RFC2822) == $request->headers->get('IF_MODIFIED_SINCE')) {
814 $response->setStatusCode(304);
815 $response->setContent('');
819 // build initial request
820 $this->request('GET', '/');
821 $this->assertHttpKernelIsCalled();
822 $this->assertEquals(200, $this->response->getStatusCode());
823 $this->assertNotNull($this->response->headers->get('Last-Modified'));
824 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
825 $this->assertEquals('Hello World', $this->response->getContent());
826 $this->assertTraceContains('miss');
827 $this->assertTraceContains('store');
828 $this->assertTraceNotContains('stale');
830 // build subsequent request; should be found but miss due to freshness
831 $this->request('GET', '/');
832 $this->assertHttpKernelIsCalled();
833 $this->assertEquals(200, $this->response->getStatusCode());
834 $this->assertNotNull($this->response->headers->get('Last-Modified'));
835 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
836 $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
837 $this->assertEquals('Hello World', $this->response->getContent());
838 $this->assertTraceContains('stale');
839 $this->assertTraceContains('valid');
840 $this->assertTraceContains('store');
841 $this->assertTraceNotContains('miss');
844 public function testValidatesCachedResponsesUseSameHttpMethod()
848 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($test) {
849 $test->assertSame('OPTIONS', $request->getMethod());
852 // build initial request
853 $this->request('OPTIONS', '/');
855 // build subsequent request
856 $this->request('OPTIONS', '/');
859 public function testValidatesCachedResponsesWithETagAndNoFreshnessInformation()
861 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
862 $response->headers->set('Cache-Control', 'public');
863 $response->headers->set('ETag', '"12345"');
864 if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) {
865 $response->setStatusCode(304);
866 $response->setContent('');
870 // build initial request
871 $this->request('GET', '/');
872 $this->assertHttpKernelIsCalled();
873 $this->assertEquals(200, $this->response->getStatusCode());
874 $this->assertNotNull($this->response->headers->get('ETag'));
875 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
876 $this->assertEquals('Hello World', $this->response->getContent());
877 $this->assertTraceContains('miss');
878 $this->assertTraceContains('store');
880 // build subsequent request; should be found but miss due to freshness
881 $this->request('GET', '/');
882 $this->assertHttpKernelIsCalled();
883 $this->assertEquals(200, $this->response->getStatusCode());
884 $this->assertNotNull($this->response->headers->get('ETag'));
885 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
886 $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
887 $this->assertEquals('Hello World', $this->response->getContent());
888 $this->assertTraceContains('stale');
889 $this->assertTraceContains('valid');
890 $this->assertTraceContains('store');
891 $this->assertTraceNotContains('miss');
894 public function testServesResponseWhileFreshAndRevalidatesWithLastModifiedInformation()
896 $time = \DateTime::createFromFormat('U', time());
898 $this->setNextResponse(200, array(), 'Hello World', function (Request $request, Response $response) use ($time) {
899 $response->setSharedMaxAge(10);
900 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
904 $this->request('GET', '/');
906 // next request before s-maxage has expired: Serve from cache
907 // without hitting the backend
908 $this->request('GET', '/');
909 $this->assertHttpKernelIsNotCalled();
910 $this->assertEquals(200, $this->response->getStatusCode());
911 $this->assertEquals('Hello World', $this->response->getContent());
912 $this->assertTraceContains('fresh');
914 sleep(15); // expire the cache
916 $this->setNextResponse(304, array(), '', function (Request $request, Response $response) use ($time) {
917 $this->assertEquals($time->format(DATE_RFC2822), $request->headers->get('IF_MODIFIED_SINCE'));
920 $this->request('GET', '/');
921 $this->assertHttpKernelIsCalled();
922 $this->assertEquals(200, $this->response->getStatusCode());
923 $this->assertEquals('Hello World', $this->response->getContent());
924 $this->assertTraceContains('stale');
925 $this->assertTraceContains('valid');
928 public function testReplacesCachedResponsesWhenValidationResultsInNon304Response()
930 $time = \DateTime::createFromFormat('U', time());
932 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time, &$count) {
933 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
934 $response->headers->set('Cache-Control', 'public');
937 $response->setContent('first response');
940 $response->setContent('second response');
943 $response->setContent('');
944 $response->setStatusCode(304);
949 // first request should fetch from backend and store in cache
950 $this->request('GET', '/');
951 $this->assertEquals(200, $this->response->getStatusCode());
952 $this->assertEquals('first response', $this->response->getContent());
954 // second request is validated, is invalid, and replaces cached entry
955 $this->request('GET', '/');
956 $this->assertEquals(200, $this->response->getStatusCode());
957 $this->assertEquals('second response', $this->response->getContent());
959 // third response is validated, valid, and returns cached entry
960 $this->request('GET', '/');
961 $this->assertEquals(200, $this->response->getStatusCode());
962 $this->assertEquals('second response', $this->response->getContent());
964 $this->assertEquals(3, $count);
967 public function testPassesHeadRequestsThroughDirectlyOnPass()
969 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
970 $response->setContent('');
971 $response->setStatusCode(200);
972 $this->assertEquals('HEAD', $request->getMethod());
975 $this->request('HEAD', '/', array('HTTP_EXPECT' => 'something ...'));
976 $this->assertHttpKernelIsCalled();
977 $this->assertEquals('', $this->response->getContent());
980 public function testUsesCacheToRespondToHeadRequestsWhenFresh()
982 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
983 $response->headers->set('Cache-Control', 'public, max-age=10');
984 $response->setContent('Hello World');
985 $response->setStatusCode(200);
986 $this->assertNotEquals('HEAD', $request->getMethod());
989 $this->request('GET', '/');
990 $this->assertHttpKernelIsCalled();
991 $this->assertEquals('Hello World', $this->response->getContent());
993 $this->request('HEAD', '/');
994 $this->assertHttpKernelIsNotCalled();
995 $this->assertEquals(200, $this->response->getStatusCode());
996 $this->assertEquals('', $this->response->getContent());
997 $this->assertEquals(\strlen('Hello World'), $this->response->headers->get('Content-Length'));
1000 public function testSendsNoContentWhenFresh()
1002 $time = \DateTime::createFromFormat('U', time());
1003 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time) {
1004 $response->headers->set('Cache-Control', 'public, max-age=10');
1005 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
1008 $this->request('GET', '/');
1009 $this->assertHttpKernelIsCalled();
1010 $this->assertEquals('Hello World', $this->response->getContent());
1012 $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
1013 $this->assertHttpKernelIsNotCalled();
1014 $this->assertEquals(304, $this->response->getStatusCode());
1015 $this->assertEquals('', $this->response->getContent());
1018 public function testInvalidatesCachedResponsesOnPost()
1020 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
1021 if ('GET' == $request->getMethod()) {
1022 $response->setStatusCode(200);
1023 $response->headers->set('Cache-Control', 'public, max-age=500');
1024 $response->setContent('Hello World');
1025 } elseif ('POST' == $request->getMethod()) {
1026 $response->setStatusCode(303);
1027 $response->headers->set('Location', '/');
1028 $response->headers->remove('Cache-Control');
1029 $response->setContent('');
1033 // build initial request to enter into the cache
1034 $this->request('GET', '/');
1035 $this->assertHttpKernelIsCalled();
1036 $this->assertEquals(200, $this->response->getStatusCode());
1037 $this->assertEquals('Hello World', $this->response->getContent());
1038 $this->assertTraceContains('miss');
1039 $this->assertTraceContains('store');
1041 // make sure it is valid
1042 $this->request('GET', '/');
1043 $this->assertHttpKernelIsNotCalled();
1044 $this->assertEquals(200, $this->response->getStatusCode());
1045 $this->assertEquals('Hello World', $this->response->getContent());
1046 $this->assertTraceContains('fresh');
1048 // now POST to same URL
1049 $this->request('POST', '/helloworld');
1050 $this->assertHttpKernelIsCalled();
1051 $this->assertEquals('/', $this->response->headers->get('Location'));
1052 $this->assertTraceContains('invalidate');
1053 $this->assertTraceContains('pass');
1054 $this->assertEquals('', $this->response->getContent());
1056 // now make sure it was actually invalidated
1057 $this->request('GET', '/');
1058 $this->assertHttpKernelIsCalled();
1059 $this->assertEquals(200, $this->response->getStatusCode());
1060 $this->assertEquals('Hello World', $this->response->getContent());
1061 $this->assertTraceContains('stale');
1062 $this->assertTraceContains('invalid');
1063 $this->assertTraceContains('store');
1066 public function testServesFromCacheWhenHeadersMatch()
1069 $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
1070 $response->headers->set('Vary', 'Accept User-Agent Foo');
1071 $response->headers->set('Cache-Control', 'public, max-age=10');
1072 $response->headers->set('X-Response-Count', ++$count);
1073 $response->setContent($request->headers->get('USER_AGENT'));
1076 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1077 $this->assertEquals(200, $this->response->getStatusCode());
1078 $this->assertEquals('Bob/1.0', $this->response->getContent());
1079 $this->assertTraceContains('miss');
1080 $this->assertTraceContains('store');
1082 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1083 $this->assertEquals(200, $this->response->getStatusCode());
1084 $this->assertEquals('Bob/1.0', $this->response->getContent());
1085 $this->assertTraceContains('fresh');
1086 $this->assertTraceNotContains('store');
1087 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
1090 public function testStoresMultipleResponsesWhenHeadersDiffer()
1093 $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
1094 $response->headers->set('Vary', 'Accept User-Agent Foo');
1095 $response->headers->set('Cache-Control', 'public, max-age=10');
1096 $response->headers->set('X-Response-Count', ++$count);
1097 $response->setContent($request->headers->get('USER_AGENT'));
1100 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1101 $this->assertEquals(200, $this->response->getStatusCode());
1102 $this->assertEquals('Bob/1.0', $this->response->getContent());
1103 $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
1105 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
1106 $this->assertEquals(200, $this->response->getStatusCode());
1107 $this->assertTraceContains('miss');
1108 $this->assertTraceContains('store');
1109 $this->assertEquals('Bob/2.0', $this->response->getContent());
1110 $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
1112 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1113 $this->assertTraceContains('fresh');
1114 $this->assertEquals('Bob/1.0', $this->response->getContent());
1115 $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
1117 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
1118 $this->assertTraceContains('fresh');
1119 $this->assertEquals('Bob/2.0', $this->response->getContent());
1120 $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
1122 $this->request('GET', '/', array('HTTP_USER_AGENT' => 'Bob/2.0'));
1123 $this->assertTraceContains('miss');
1124 $this->assertEquals('Bob/2.0', $this->response->getContent());
1125 $this->assertEquals(3, $this->response->headers->get('X-Response-Count'));
1128 public function testShouldCatchExceptions()
1130 $this->catchExceptions();
1132 $this->setNextResponse();
1133 $this->request('GET', '/');
1135 $this->assertExceptionsAreCaught();
1138 public function testShouldCatchExceptionsWhenReloadingAndNoCacheRequest()
1140 $this->catchExceptions();
1142 $this->setNextResponse();
1143 $this->cacheConfig['allow_reload'] = true;
1144 $this->request('GET', '/', array(), array(), false, array('Pragma' => 'no-cache'));
1146 $this->assertExceptionsAreCaught();
1149 public function testShouldNotCatchExceptions()
1151 $this->catchExceptions(false);
1153 $this->setNextResponse();
1154 $this->request('GET', '/');
1156 $this->assertExceptionsAreNotCaught();
1159 public function testEsiCacheSendsTheLowestTtl()
1164 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1166 'Cache-Control' => 's-maxage=300',
1167 'Surrogate-Control' => 'content="ESI/1.0"',
1172 'body' => 'Hello World!',
1173 'headers' => array('Cache-Control' => 's-maxage=200'),
1177 'body' => 'My name is Bobby.',
1178 'headers' => array('Cache-Control' => 's-maxage=100'),
1182 $this->setNextResponses($responses);
1184 $this->request('GET', '/', array(), array(), true);
1185 $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
1187 $this->assertEquals(100, $this->response->getTtl());
1190 public function testEsiCacheSendsTheLowestTtlForHeadRequests()
1195 'body' => 'I am a long-lived master response, but I embed a short-lived resource: <esi:include src="/foo" />',
1197 'Cache-Control' => 's-maxage=300',
1198 'Surrogate-Control' => 'content="ESI/1.0"',
1203 'body' => 'I am a short-lived resource',
1204 'headers' => array('Cache-Control' => 's-maxage=100'),
1208 $this->setNextResponses($responses);
1210 $this->request('HEAD', '/', array(), array(), true);
1212 $this->assertEmpty($this->response->getContent());
1213 $this->assertEquals(100, $this->response->getTtl());
1216 public function testEsiCacheForceValidation()
1221 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1223 'Cache-Control' => 's-maxage=300',
1224 'Surrogate-Control' => 'content="ESI/1.0"',
1229 'body' => 'Hello World!',
1230 'headers' => array('ETag' => 'foobar'),
1234 'body' => 'My name is Bobby.',
1235 'headers' => array('Cache-Control' => 's-maxage=100'),
1239 $this->setNextResponses($responses);
1241 $this->request('GET', '/', array(), array(), true);
1242 $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
1243 $this->assertNull($this->response->getTtl());
1244 $this->assertTrue($this->response->mustRevalidate());
1245 $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
1246 $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
1249 public function testEsiCacheForceValidationForHeadRequests()
1254 'body' => 'I am the master response and use expiration caching, but I embed another resource: <esi:include src="/foo" />',
1256 'Cache-Control' => 's-maxage=300',
1257 'Surrogate-Control' => 'content="ESI/1.0"',
1262 'body' => 'I am the embedded resource and use validation caching',
1263 'headers' => array('ETag' => 'foobar'),
1267 $this->setNextResponses($responses);
1269 $this->request('HEAD', '/', array(), array(), true);
1271 // The response has been assembled from expiration and validation based resources
1272 // This can neither be cached nor revalidated, so it should be private/no cache
1273 $this->assertEmpty($this->response->getContent());
1274 $this->assertNull($this->response->getTtl());
1275 $this->assertTrue($this->response->mustRevalidate());
1276 $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
1277 $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
1280 public function testEsiRecalculateContentLengthHeader()
1285 'body' => '<esi:include src="/foo" />',
1287 'Content-Length' => 26,
1288 'Surrogate-Control' => 'content="ESI/1.0"',
1293 'body' => 'Hello World!',
1294 'headers' => array(),
1298 $this->setNextResponses($responses);
1300 $this->request('GET', '/', array(), array(), true);
1301 $this->assertEquals('Hello World!', $this->response->getContent());
1302 $this->assertEquals(12, $this->response->headers->get('Content-Length'));
1305 public function testEsiRecalculateContentLengthHeaderForHeadRequest()
1310 'body' => '<esi:include src="/foo" />',
1312 'Content-Length' => 26,
1313 'Surrogate-Control' => 'content="ESI/1.0"',
1318 'body' => 'Hello World!',
1319 'headers' => array(),
1323 $this->setNextResponses($responses);
1325 $this->request('HEAD', '/', array(), array(), true);
1327 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13
1328 // "The Content-Length entity-header field indicates the size of the entity-body,
1329 // in decimal number of OCTETs, sent to the recipient or, in the case of the HEAD
1330 // method, the size of the entity-body that would have been sent had the request
1332 $this->assertEmpty($this->response->getContent());
1333 $this->assertEquals(12, $this->response->headers->get('Content-Length'));
1336 public function testClientIpIsAlwaysLocalhostForForwardedRequests()
1338 $this->setNextResponse();
1339 $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1341 $this->kernel->assert(function ($backendRequest) {
1342 $this->assertSame('127.0.0.1', $backendRequest->server->get('REMOTE_ADDR'));
1347 * @dataProvider getTrustedProxyData
1349 public function testHttpCacheIsSetAsATrustedProxy(array $existing)
1351 Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_ALL);
1353 $this->setNextResponse();
1354 $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1355 $this->assertSame($existing, Request::getTrustedProxies());
1357 $existing = array_unique(array_merge($existing, array('127.0.0.1')));
1358 $this->kernel->assert(function ($backendRequest) use ($existing) {
1359 $this->assertSame($existing, Request::getTrustedProxies());
1360 $this->assertsame('10.0.0.1', $backendRequest->getClientIp());
1363 Request::setTrustedProxies(array(), -1);
1366 public function getTrustedProxyData()
1370 array(array('10.0.0.2')),
1371 array(array('10.0.0.2', '127.0.0.1')),
1376 * @dataProvider getForwardedData
1378 public function testForwarderHeaderForForwardedRequests($forwarded, $expected)
1380 $this->setNextResponse();
1381 $server = array('REMOTE_ADDR' => '10.0.0.1');
1382 if (null !== $forwarded) {
1383 Request::setTrustedProxies($server, -1);
1384 $server['HTTP_FORWARDED'] = $forwarded;
1386 $this->request('GET', '/', $server);
1388 $this->kernel->assert(function ($backendRequest) use ($expected) {
1389 $this->assertSame($expected, $backendRequest->headers->get('Forwarded'));
1392 Request::setTrustedProxies(array(), -1);
1395 public function getForwardedData()
1398 array(null, 'for="10.0.0.1";host="localhost";proto=http'),
1399 array('for=10.0.0.2', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.1"'),
1400 array('for=10.0.0.2, for=10.0.0.3', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.3", for="10.0.0.1"'),
1404 public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses()
1406 $time = \DateTime::createFromFormat('U', time());
1411 'body' => '<esi:include src="/hey" />',
1413 'Surrogate-Control' => 'content="ESI/1.0"',
1415 'Last-Modified' => $time->format(DATE_RFC2822),
1421 'headers' => array(),
1425 $this->setNextResponses($responses);
1427 $this->request('GET', '/', array(), array(), true);
1428 $this->assertNull($this->response->getETag());
1429 $this->assertNull($this->response->getLastModified());
1432 public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponsesAndHeadRequest()
1434 $time = \DateTime::createFromFormat('U', time());
1439 'body' => '<esi:include src="/hey" />',
1441 'Surrogate-Control' => 'content="ESI/1.0"',
1443 'Last-Modified' => $time->format(DATE_RFC2822),
1449 'headers' => array(),
1453 $this->setNextResponses($responses);
1455 $this->request('HEAD', '/', array(), array(), true);
1456 $this->assertEmpty($this->response->getContent());
1457 $this->assertNull($this->response->getETag());
1458 $this->assertNull($this->response->getLastModified());
1461 public function testDoesNotCacheOptionsRequest()
1463 $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'get');
1464 $this->request('GET', '/');
1465 $this->assertHttpKernelIsCalled();
1467 $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'options');
1468 $this->request('OPTIONS', '/');
1469 $this->assertHttpKernelIsCalled();
1471 $this->request('GET', '/');
1472 $this->assertHttpKernelIsNotCalled();
1473 $this->assertSame('get', $this->response->getContent());
1476 public function testUsesOriginalRequestForSurrogate()
1478 $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
1479 $store = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpCache\StoreInterface')->getMock();
1482 ->expects($this->exactly(2))
1484 ->willReturnCallback(function (Request $request) {
1485 $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));
1487 return new Response();
1490 $cache = new HttpCache($kernel,
1495 $request = Request::create('/');
1496 $request->server->set('REMOTE_ADDR', '10.0.0.1');
1499 $cache->handle($request, HttpKernelInterface::MASTER_REQUEST);
1501 // Main request was now modified by HttpCache
1502 // The surrogate will ask for the request using $this->cache->getRequest()
1503 // which MUST return the original request so the surrogate
1504 // can actually behave like a reverse proxy like e.g. Varnish would.
1505 $this->assertSame('10.0.0.1', $cache->getRequest()->getClientIp());
1506 $this->assertSame('10.0.0.1', $cache->getRequest()->server->get('REMOTE_ADDR'));
1508 // Surrogate request
1509 $cache->handle($request, HttpKernelInterface::SUB_REQUEST);
1513 class TestKernel implements HttpKernelInterface
1515 public $terminateCalled = false;
1517 public function terminate(Request $request, Response $response)
1519 $this->terminateCalled = true;
1522 public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)