【问题标题】:Symfony returning HTTP 304 locally, but HTTP 200 for matching ETag in productionSymfony 在本地返回 HTTP 304,但在生产中匹配 ETag 时返回 HTTP 200
【发布时间】:2022-01-10 16:47:12
【问题描述】:

由于需要 20 秒以上的繁重 API 调用,我已启用该资源的 ETag 计算,并在执行任何繁重的数据库工作之前预先计算 ETag,以便能够提前返回。使用 PHP 开发服务器可以正常工作(我可以调试代码并查看 HTTP 状态代码是否正确)并且我得到 HTTP 304,但由于某种原因,它在部署到生产环境时似乎没有生效,我想知道如何可以调试吗?

curl 调用看到正确的 If-None-Match ETag 正在发送,但我仍然得到 HTTP 200 和相同的 Etag 而不是 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"

由于这在本地工作(使用 Symfony 开发服务器:./bin/console server:run),但在部署到生产环境时不行,我怀疑这与 $kernel 正在运行有关,但我遵循了 caching guide 和更改了 web/app.phpweb/app_dev.php 以包含以下内容:

$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();

知道什么地方出了问题吗?


有问题的代码如下所示:

$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;

由于在 Symfony 中启用 HttpCache 类会在它们到达 AppKernel 之前删除所有缓存头,因此使用这种“提前退出”通常是不可能的,但我选择使用一个小技巧来获取 AppCache.php 中的头和将它们分配给自定义标题 (X-If-Modified-Since),然后我可以在上面的 etag 比较中进行比较:

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);
    }

在生产中运行

  • Nginx 后面的 PHP FPM。
  • Symfony 3.4
  • PHP 7.2

【问题讨论】:

    标签: php symfony symfony-3.4 http-caching


    【解决方案1】:

    最后,我最终通过 HttpCache 逐步调试,发现这实际上是 issue #37948 on the Symfony tracker尝试不到一年前在 March 2021 中修复。 p>

    一些浏览器在 etags 中添加了 W/(弱)前缀,而 Symfony 根本没有处理这个问题。所以通常它会在响应中设置Etag: "foo",并且它无法看到这与来自浏览器的If-None-Match: W/"foo" 中的etag 相同,这与RFC 规范所说的不同。

    因此,为了满足我的需要,我实际上需要手动解析控制器代码中的 etag 以修补逻辑。这就是我不升级的结果,我猜 :) 补丁代码可以在Response.php code for Symfony 4.4 中找到。

    这段代码是我使用的,基本上是 Symfony 4.4 的 PR,在比较之前对实体标签进行了规范化。

    protected static function normalizeWeakComparitor($etag)
    {
        if (self::isWeakComparitor($etag)) {
            return self::stripQuotes(substr($etag, 2));
        }
        return self::stripQuotes($etag);
    }
    
    protected static function isWeakComparitor($ifNoneMatchEtag): bool
    {
        return 0 == strncmp($ifNoneMatchEtag, 'W/', 2);
    }
    
    protected static function stripQuotes($string)
    {
        if (substr($string, 0, 1) == '"' && substr($string, -1) == '"') {
            return substr($string, 1, -1);
        }
        return $string;
    }
    
    /**
     * Manual matching code to handle a bug in the ETag code in Symfony <= 4.4
     * See https://stackoverflow.com/questions/70655974/symfony-returning-http-304-locally-but-http-200-for-matching-etag-in-production/70670045#70670045
     *
     * @param Request $request
     * @param $etag
     * @return bool
     */
    protected static function matchingEtag(Request $request, $etag): bool
    {
        $notModified = false;
        $ifNoneMatchEtags = preg_split('/\s*,\s*/', $request->headers->get(\AppCache::CUSTOM_IF_NONE_MATCH_HEADER), null, PREG_SPLIT_NO_EMPTY);
    
        if ($ifNoneMatchEtags) {
    
            // Based on buggy patch to Symfony 4.4 I fixed up
            $etag = self::normalizeWeakComparitor($etag);
    
            // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2.
            foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) {
                $ifNoneMatchEtag = self::normalizeWeakComparitor($ifNoneMatchEtag);
    
                if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) {
                    $notModified = true;
                    break;
                }
            }
        }
        return $notModified;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-10-30
      • 1970-01-01
      • 2017-03-22
      • 2014-01-24
      • 2021-01-23
      • 1970-01-01
      • 1970-01-01
      • 2015-11-22
      相关资源
      最近更新 更多