Include Cache-Control header on 301 redirects.

Created on 15 May 2019, over 5 years ago
Updated 12 April 2024, 7 months ago

Problem/Motivation

This issue is also being discussed in the Redirect issue queue #3016776: 301 redirects don't contain cache-control header β†’ .

TrustedRedirectResponse implements CacheableResponseInterface (by extending from SecuredRedirectResponse). When caching is enabled, and the redirect response status code is anything other than 301, a Cache-Control header is output. However, when the status code is 301 (permanent redirect), no Cache-Control header is emitted.

The lack of a cache-control header on 301 redirect responses means that intermediate CDNs and reverse proxies either don't cache the response at all (for example Akamai respects Cache-Control), or fall back to caching it for a short period (for example Acquia Varnish). Some browsers cache 301 redirects if Cache-Control is missing.

In order for a response to be cached in FinishResponseSubscriber, \Drupal\Core\EventSubscriber\FinishResponseSubscriber::isCacheControlCustomized() has to return FALSE.

  /**
   * Determine whether the given response has a custom Cache-Control header.
   *
   * Upon construction, the ResponseHeaderBag is initialized with an empty
   * Cache-Control header. Consequently it is not possible to check whether the
   * header was set explicitly by simply checking its presence. Instead, it is
   * necessary to examine the computed Cache-Control header and compare with
   * values known to be present only when Cache-Control was never set
   * explicitly.
   *
   * When neither Cache-Control nor any of the ETag, Last-Modified, Expires
   * headers are set on the response, ::get('Cache-Control') returns the value
   * 'no-cache, private'. If any of ETag, Last-Modified or Expires are set but
   * not Cache-Control, then 'private, must-revalidate' (in exactly this order)
   * is returned.
   *
   * @see \Symfony\Component\HttpFoundation\ResponseHeaderBag::computeCacheControlValue()
   *
   * @param \Symfony\Component\HttpFoundation\Response $response
   *
   * @return bool
   *   TRUE when Cache-Control header was set explicitly on the given response.
   */
  protected function isCacheControlCustomized(Response $response) {
    $cache_control = $response->headers->get('Cache-Control');
    return $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate';
  }

Drupal's current behavior was introduced with a change to Symfony\Component\HttpFoundation\RedirectResponse in Symfony 3.2 (Symfony issue). The constructor for RedirectResponse explicitly unsets the Cache-Control header, causing isCacheControlCustomized() to return TRUE, which renders the response not cacheable.

Proposed resolution

Emitting the Cache-Control header for 301 responses would make them cacheable downstream. It also does not make sense to have the temporary redirects (302) be cacheable, while the permanent redirects (301) are not (or at least not explicit).

Initial attempt attached to output Cache-Control on 301 responses by checking if the cache-control header was unset by Symfony.

Remaining tasks

User interface changes

None.

API changes

None.

Data model changes

None.

πŸ› Bug report
Status

Fixed

Version

10.3 ✨

Component
Request processingΒ  β†’

Last updated 3 days ago

No maintainer
Created by

πŸ‡§πŸ‡ͺBelgium mr.baileys πŸ‡§πŸ‡ͺ (Ghent)

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • The Needs Review Queue Bot β†’ tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

    Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

    Consult the Drupal Contributor Guide β†’ to find step-by-step guides for working with issues.

  • πŸ‡©πŸ‡ͺGermany Anybody Porta Westfalica

    The upstream Symfony issue from #20 is fixed, so we could proceed here, but now it seems unclear, what's the status in Drupal 10. Is the issue still existing and is someone still using a patch from this issue?

    MAYBE things might get worse if this is not fixed and we merge ✨ .htaccess ExpiresDefault (2W) is much too low. Should be ~1Y Fixed ?

  • πŸ‡ΊπŸ‡ΈUnited States bkosborne New Jersey, USA

    This is indeed still an issue in Drupal 10. And indeed, this patch can be simplified as we should no longer need to deal with the different case of the cache-control header anymore.

  • πŸ‡¦πŸ‡ΊAustralia sime Melbourne

    Vanilla re-roll of #27 (9.x) against 10.1.x

  • πŸ‡¦πŸ‡ΊAustralia sime Melbourne

    A vanilla re-roll of the patch for 11.x and running tests.

  • last update over 1 year ago
    Custom Commands Failed
  • πŸ‡¦πŸ‡ΊAustralia sime Melbourne

    Lint

  • last update over 1 year ago
    Custom Commands Failed
  • πŸ‡¦πŸ‡ΊAustralia sime Melbourne

    Lint, passing tests

  • last update over 1 year ago
    29,402 pass
  • πŸ‡¦πŸ‡ΊAustralia sime Melbourne

    So that's passing, but needs reviewing feedback of #29 in #38 given upstream changes in Synfony.

  • Status changed to Needs review 11 months ago
  • πŸ‡¦πŸ‡ΊAustralia pameeela

    Patch applies still, setting to NR.

  • Status changed to Needs work 11 months ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Nitpicky stuff, can we add typehints and returns for new functions and parameters through.

    Converting to an MR is also preferred if possibe

  • First commit to issue fork.
  • Status changed to Needs review 11 months ago
  • πŸ‡ΊπŸ‡ΈUnited States angrytoast Princeton, NJ

    Updated with a MR and incorporates feedback from #29 regarding no need for the case-insensitive header logic update.

    Also updated with typehints for params + returns on affected / new methods as requested by #46.

  • πŸ‡§πŸ‡ͺBelgium mr.baileys πŸ‡§πŸ‡ͺ (Ghent)
  • Pipeline finished with Success
    11 months ago
    Total: 505694s
    #68893
  • Status changed to RTBC 11 months ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave
    There was 1 error:
    1) Drupal\Tests\page_cache\Functional\PageCacheTest::testCacheabilityOfRedirectResponses
    Behat\Mink\Exception\ExpectationException: Current response header "Cache-Control" is "", but "max-age=300, public" expected.
    /builds/issue/drupal-3054821/vendor/behat/mink/src/WebAssert.php:794
    /builds/issue/drupal-3054821/vendor/behat/mink/src/WebAssert.php:161
    /builds/issue/drupal-3054821/core/modules/page_cache/tests/src/Functional/PageCacheTest.php:577
    /builds/issue/drupal-3054821/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
    ERRORS!
    Tests: 14, Assertions: 183, Errors: 1.
    PHPUnit 9.6.15 by Sebastian Bergmann and contributors.
    Testing Drupal\Tests\Component\HttpFoundation\SecuredRedirectResponseTest
    .                                                                   1 / 1 (100%)
    

    Test-only feature showed test-coverage.

    Appears all feedback has been addressed.

    Believe this is good.

  • Status changed to Needs work 9 months ago
  • πŸ‡³πŸ‡ΏNew Zealand quietone

    This needs a rebase.

  • πŸ‡¬πŸ‡§United Kingdom pobster

    It needs a bit of rewriting now too, as we have AssertPageCacheContextsAndTagsTrait for enabling and checking headers. Might as well use it.

    If I get time later on I'll do it...

  • πŸ‡§πŸ‡ͺBelgium Tim Lammar

    Copy of patch #40 but with correct extension (without the '.txt').
    Looks like this patch would be able to work on D10.2.x

  • πŸ‡§πŸ‡ͺBelgium Tim Lammar

    apparently my previous patch (rename from #40) would not apply to 10.2.x
    Made some changes and this one should.

  • πŸ‡¬πŸ‡§United Kingdom pobster

    As I was saying in #53, we have AssertPageCacheContextsAndTagsTrait available here - how would you feel about changing the test from say;

    /**
     * Verify that the cache-control header is added by FinishResponseSubscriber.
     */
    public function testCacheabilityOfRedirectResponses() {
      $config = $this->config('system.performance');
      $config->set('cache.page.max_age', 300);
      $config->save();
    
      $this->getSession()->getDriver()->getClient()->followRedirects(FALSE);
      $this->maximumMetaRefreshCount = 0;
    
      foreach ([301, 302, 303, 307, 308] as $status_code) {
        foreach (['local', 'cacheable', 'trusted'] as $type) {
          $this->drupalGet("/system-test/redirect/${type}/${status_code}");
          $this->assertResponse($status_code);
          $this->assertHeader('Cache-Control', 'max-age=300, public');
        }
      }
    }
    

    To something like;

    /**
     * Verify that the cache-control header is added by FinishResponseSubscriber.
     */
    public function testCacheabilityOfRedirectResponses() {
      $this->enablePageCaching();
    
      $this->getSession()->getDriver()->getClient()->followRedirects(FALSE);
      $this->maximumMetaRefreshCount = 0;
    
      foreach ([301, 302, 303, 307, 308] as $status_code) {
        foreach (['local', 'cacheable', 'trusted'] as $type) {
          $this->drupalGet("/system-test/redirect/${type}/${status_code}");
          $this->assertResponse($status_code);
          $this->assertCacheMaxAge(300);
        }
      }
    }
    
  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    #56 makes sense to me, we have those helpers so why not use them.

  • πŸ‡³πŸ‡ΏNew Zealand wiifm Wellington, NZ

    wiifm β†’ changed the visibility of the branch 3054821-include-cache-control-header to hidden.

  • πŸ‡³πŸ‡ΏNew Zealand wiifm Wellington, NZ

    wiifm β†’ changed the visibility of the branch 3054821-include-cache-control-header to active.

  • Pipeline finished with Failed
    8 months ago
    Total: 183s
    #125675
  • πŸ‡¦πŸ‡ΊAustralia RichardGaunt Melbourne

    Patches rolled into the MR by Sean, hiding patches.

  • πŸ‡¦πŸ‡ΊAustralia RichardGaunt Melbourne
  • Pipeline finished with Canceled
    8 months ago
    Total: 111s
    #125678
  • Pipeline finished with Canceled
    8 months ago
    Total: 135s
    #125681
  • Pipeline finished with Failed
    8 months ago
    Total: 563s
    #125685
  • Pipeline finished with Success
    8 months ago
    Total: 618s
    #125693
  • πŸ‡³πŸ‡ΏNew Zealand quietone

    I read the MR with wiifm, focusing on the comments. I noticed different casing of 'Cache-Control' and the tense of a verb that should change. Those are being looked into. The code changes read well to me and are easy to understand, however I can't RTBC. I asked if the change was specific to a version of Symfony which he did not think so.

  • Pipeline finished with Success
    8 months ago
    Total: 495s
    #125727
  • πŸ‡³πŸ‡ΏNew Zealand wiifm Wellington, NZ
  • Status changed to Needs review 8 months ago
  • πŸ‡³πŸ‡ΏNew Zealand wiifm Wellington, NZ
  • πŸ‡³πŸ‡ΏNew Zealand wiifm Wellington, NZ
  • Pipeline finished with Success
    8 months ago
    Total: 483s
    #125785
  • Pipeline finished with Success
    8 months ago
    Total: 555s
    #125799
  • πŸ‡¦πŸ‡ΊAustralia acbramley

    Adding credit for contributors during the DS2024 Code sprint.

    Just 1 final question left on the MR.

  • Pipeline finished with Success
    8 months ago
    Total: 613s
    #128686
  • Status changed to RTBC 8 months ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Reverted change to SecuredRedirectResponse and tests still pass so don't believe it was needed. That resolved the last thread I believe.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Looks like a well-tested and tightly scoped change, with potentially big scalability improvements (far fewer origin requests to respond to)!

  • πŸ‡¬πŸ‡§United Kingdom alexpott πŸ‡ͺπŸ‡ΊπŸŒ

    Committed and pushed 2fbe22aa19 to 11.x and ec45fd5604 to 10.3.x. Thanks!

  • Status changed to Fixed 8 months ago
  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024