简体   繁体   中英

Symfony returning HTTP 304 locally, but HTTP 200 for matching ETag in production

Due to a heavy API call taking upwards of 20 seconds, I have enabled computation of ETag for that resource and pre-compute the ETag before doing any heavy DB work to be able to return early. This works fine using the PHP development server (I can debug step through the code and see the HTTP status codes being correct) and I get HTTP 304, but for some reason it does not seem to take effect when deploying to production and I wonder how I can debug it?

The curl call sees the right If-None-Match ETag being sent, but still I get HTTP 200 and the same Etag back instead of HTTP 304.

> if-none-match: W/"5e3f549935516374f93531655b66aaf4"
< HTTP/2 200
< date: Mon, 10 Jan 2022 16:21:55 GMT
< content-type: application/json
< server: nginx
< cache-control: private
< etag: W/"5e3f549935516374f93531655b66aaf4"

As this works locally (using the Symfony development server: ./bin/console server:run ), but not when deploying to production, I suspect it is something to do with which $kernel is running, but I have followed the caching guide and changed both web/app.php and web/app_dev.php to include the following:

$kernel = new AppKernel('prod', false);

// enable caching
// all authorized endpoints are automatically no-cache by default
$kernel = new AppCache($kernel);
// When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter
Request::enableHttpMethodParameterOverride();

Any idea on what could be wrong?


The code in question looks something like this:

$computed = $this->getEtagAndTimestampForCatalogByUserAndType($user, $catalog_type);
$etag = $computed['etag'];
// Save the DB some work and avoid recomputing already here
if (self::matchingEtag($request, $etag)) {
    return $this->json(null, Response::HTTP_NOT_MODIFIED);
}

// bla bla domain code
$responseData = doHeavyDBStuff();

$jsonResponse = $this->json($responseData);
$jsonResponse->setEtag($etag);

return $jsonResponse;

Since enabling the HttpCache class in Symfony removes all caching headers before they reach AppKernel using this "early-exit" is not usually possible, but I chose to use a little trick where I fetch the headers in AppCache.php and assign them to custom headers ( X-If-Modified-Since ) that I then can compare in my etag comparison above:

class AppCache extends HttpCache
{
    const CUSTOM_IF_NONE_MATCH_HEADER = 'x-if_none_match';

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        /* Work around the fact that the caching layer removes the headers. This allows for custom caching strategies */
        $request->headers->add(array(AppCache::CUSTOM_IF_NONE_MATCH_HEADER => $request->headers->get('if_none_match')));
        return parent::handle($request, $type, $catch);
    }

In production this is running on

  • PHP FPM behind Nginx.
  • Symfony 3.4
  • PHP 7.2

In the end I ended up debugging this step-by-step through HttpCache and found this is actually issue #37948 on the Symfony tracker which was only fixed less than a year ago, in March 2021 .

Some browsers add the W/ (weak) prefix to the etags and Symfony did not handle this at all. So typically it sets Etag: "foo" on the response and it was unable to see that this was the same as the etag in If-None-Match: W/"foo" coming from the browser.

So to make this work for my needs, I actually needed to hand-parse the etags in my controller code to patch the logic. That's what I get for not upgrading, I guess:) The required patching code can be found in the Response.php code for Symfony 4.4 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM