* the cache can serve a stale response when an error is encountered (default: 60).
* This setting is overridden by the stale-if-error HTTP Cache-Control extension
* (see RFC 5861).
- *
- * @param HttpKernelInterface $kernel An HttpKernelInterface instance
- * @param StoreInterface $store A Store instance
- * @param SurrogateInterface $surrogate A SurrogateInterface instance
- * @param array $options An array of options
*/
public function __construct(HttpKernelInterface $kernel, StoreInterface $store, SurrogateInterface $surrogate = null, array $options = array())
{
}
}
- $path = $request->getPathInfo();
- if ($qs = $request->getQueryString()) {
- $path .= '?'.$qs;
- }
- $this->traces[$request->getMethod().' '.$path] = array();
+ $this->traces[$this->getTraceKey($request)] = array();
if (!$request->isMethodSafe(false)) {
$response = $this->invalidate($request, $catch);
} elseif ($request->headers->has('expect') || !$request->isMethodCacheable()) {
$response = $this->pass($request, $catch);
+ } elseif ($this->options['allow_reload'] && $request->isNoCache()) {
+ /*
+ If allow_reload is configured and the client requests "Cache-Control: no-cache",
+ reload the cache by fetching a fresh response and caching it (if possible).
+ */
+ $this->record($request, 'reload');
+ $response = $this->fetch($request, $catch);
} else {
$response = $this->lookup($request, $catch);
}
$this->restoreResponseBody($request, $response);
- $response->setDate(\DateTime::createFromFormat('U', time(), new \DateTimeZone('UTC')));
-
if (HttpKernelInterface::MASTER_REQUEST === $type && $this->options['debug']) {
$response->headers->set('X-Symfony-Cache', $this->getLog());
}
* it triggers "miss" processing.
*
* @param Request $request A Request instance
- * @param bool $catch whether to process exceptions
+ * @param bool $catch Whether to process exceptions
*
* @return Response A Response instance
*
*/
protected function lookup(Request $request, $catch = false)
{
- // if allow_reload and no-cache Cache-Control, allow a cache reload
- if ($this->options['allow_reload'] && $request->isNoCache()) {
- $this->record($request, 'reload');
-
- return $this->fetch($request, $catch);
- }
-
try {
$entry = $this->store->lookup($request);
} catch (\Exception $e) {
}
/**
- * Forwards the Request to the backend and determines whether the response should be stored.
- *
- * This methods is triggered when the cache missed or a reload is required.
+ * Unconditionally fetches a fresh response from the backend and
+ * stores it in the cache if is cacheable.
*
* @param Request $request A Request instance
- * @param bool $catch whether to process exceptions
+ * @param bool $catch Whether to process exceptions
*
* @return Response A Response instance
*/
/**
* Forwards the Request to the backend and returns the Response.
*
+ * All backend requests (cache passes, fetches, cache validations)
+ * run through this method.
+ *
* @param Request $request A Request instance
* @param bool $catch Whether to catch exceptions or not
* @param Response $entry A Response instance (the stale entry if present, null otherwise)
// make sure HttpCache is a trusted proxy
if (!in_array('127.0.0.1', $trustedProxies = Request::getTrustedProxies())) {
$trustedProxies[] = '127.0.0.1';
- Request::setTrustedProxies($trustedProxies, method_exists('Request', 'getTrustedHeaderSet') ? Request::getTrustedHeaderSet() : -1);
+ Request::setTrustedProxies($trustedProxies, Request::HEADER_X_FORWARDED_ALL);
}
// always a "master" request (as the real master request can be in cache)
}
}
+ /*
+ RFC 7231 Sect. 7.1.1.2 says that a server that does not have a reasonably accurate
+ clock MUST NOT send a "Date" header, although it MUST send one in most other cases
+ except for 1xx or 5xx responses where it MAY do so.
+
+ Anyway, a client that received a message without a "Date" header MUST add it.
+ */
+ if (!$response->headers->has('Date')) {
+ $response->setDate(\DateTime::createFromFormat('U', time()));
+ }
+
$this->processResponseBody($request, $response);
if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) {
/**
* Checks whether the cache entry is "fresh enough" to satisfy the Request.
*
- * @param Request $request A Request instance
- * @param Response $entry A Response instance
- *
* @return bool true if the cache entry if fresh enough, false otherwise
*/
protected function isFreshEnough(Request $request, Response $entry)
/**
* Locks a Request during the call to the backend.
*
- * @param Request $request A Request instance
- * @param Response $entry A Response instance
- *
* @return bool true if the cache entry can be returned even if it is staled, false otherwise
*/
protected function lock(Request $request, Response $entry)
// try to acquire a lock to call the backend
$lock = $this->store->lock($request);
- // there is already another process calling the backend
- if (true !== $lock) {
- // check if we can serve the stale entry
- if (null === $age = $entry->headers->getCacheControlDirective('stale-while-revalidate')) {
- $age = $this->options['stale_while_revalidate'];
- }
+ if (true === $lock) {
+ // we have the lock, call the backend
+ return false;
+ }
- if (abs($entry->getTtl()) < $age) {
- $this->record($request, 'stale-while-revalidate');
+ // there is already another process calling the backend
- // server the stale response while there is a revalidation
- return true;
- }
+ // May we serve a stale response?
+ if ($this->mayServeStaleWhileRevalidate($entry)) {
+ $this->record($request, 'stale-while-revalidate');
- // wait for the lock to be released
- $wait = 0;
- while ($this->store->isLocked($request) && $wait < 5000000) {
- usleep(50000);
- $wait += 50000;
- }
+ return true;
+ }
- if ($wait < 5000000) {
- // replace the current entry with the fresh one
- $new = $this->lookup($request);
- $entry->headers = $new->headers;
- $entry->setContent($new->getContent());
- $entry->setStatusCode($new->getStatusCode());
- $entry->setProtocolVersion($new->getProtocolVersion());
- foreach ($new->headers->getCookies() as $cookie) {
- $entry->headers->setCookie($cookie);
- }
- } else {
- // backend is slow as hell, send a 503 response (to avoid the dog pile effect)
- $entry->setStatusCode(503);
- $entry->setContent('503 Service Unavailable');
- $entry->headers->set('Retry-After', 10);
+ // wait for the lock to be released
+ if ($this->waitForLock($request)) {
+ // replace the current entry with the fresh one
+ $new = $this->lookup($request);
+ $entry->headers = $new->headers;
+ $entry->setContent($new->getContent());
+ $entry->setStatusCode($new->getStatusCode());
+ $entry->setProtocolVersion($new->getProtocolVersion());
+ foreach ($new->headers->getCookies() as $cookie) {
+ $entry->headers->setCookie($cookie);
}
-
- return true;
+ } else {
+ // backend is slow as hell, send a 503 response (to avoid the dog pile effect)
+ $entry->setStatusCode(503);
+ $entry->setContent('503 Service Unavailable');
+ $entry->headers->set('Retry-After', 10);
}
- // we have the lock, call the backend
- return false;
+ return true;
}
/**
* Writes the Response to the cache.
*
- * @param Request $request A Request instance
- * @param Response $response A Response instance
- *
* @throws \Exception
*/
protected function store(Request $request, Response $response)
{
- if (!$response->headers->has('Date')) {
- $response->setDate(\DateTime::createFromFormat('U', time()));
- }
try {
$this->store->write($request, $response);
/**
* Restores the Response body.
- *
- * @param Request $request A Request instance
- * @param Response $response A Response instance
*/
private function restoreResponseBody(Request $request, Response $response)
{
- if ($request->isMethod('HEAD') || 304 === $response->getStatusCode()) {
- $response->setContent(null);
- $response->headers->remove('X-Body-Eval');
- $response->headers->remove('X-Body-File');
-
- return;
- }
-
if ($response->headers->has('X-Body-Eval')) {
ob_start();
$response->headers->set('Content-Length', strlen($response->getContent()));
}
} elseif ($response->headers->has('X-Body-File')) {
- $response->setContent(file_get_contents($response->headers->get('X-Body-File')));
+ // Response does not include possibly dynamic content (ESI, SSI), so we need
+ // not handle the content for HEAD requests
+ if (!$request->isMethod('HEAD')) {
+ $response->setContent(file_get_contents($response->headers->get('X-Body-File')));
+ }
} else {
return;
}
* Checks if the Request includes authorization or other sensitive information
* that should cause the Response to be considered private by default.
*
- * @param Request $request A Request instance
- *
* @return bool true if the Request is private, false otherwise
*/
private function isPrivateRequest(Request $request)
* @param string $event The event name
*/
private function record(Request $request, $event)
+ {
+ $this->traces[$this->getTraceKey($request)][] = $event;
+ }
+
+ /**
+ * Calculates the key we use in the "trace" array for a given request.
+ *
+ * @param Request $request
+ *
+ * @return string
+ */
+ private function getTraceKey(Request $request)
{
$path = $request->getPathInfo();
if ($qs = $request->getQueryString()) {
$path .= '?'.$qs;
}
- $this->traces[$request->getMethod().' '.$path][] = $event;
+
+ return $request->getMethod().' '.$path;
+ }
+
+ /**
+ * Checks whether the given (cached) response may be served as "stale" when a revalidation
+ * is currently in progress.
+ *
+ * @param Response $entry
+ *
+ * @return bool true when the stale response may be served, false otherwise
+ */
+ private function mayServeStaleWhileRevalidate(Response $entry)
+ {
+ $timeout = $entry->headers->getCacheControlDirective('stale-while-revalidate');
+
+ if (null === $timeout) {
+ $timeout = $this->options['stale_while_revalidate'];
+ }
+
+ return abs($entry->getTtl()) < $timeout;
+ }
+
+ /**
+ * Waits for the store to release a locked entry.
+ *
+ * @param Request $request The request to wait for
+ *
+ * @return bool true if the lock was released before the internal timeout was hit; false if the wait timeout was exceeded
+ */
+ private function waitForLock(Request $request)
+ {
+ $wait = 0;
+ while ($this->store->isLocked($request) && $wait < 100) {
+ usleep(50000);
+ ++$wait;
+ }
+
+ return $wait < 100;
}
}