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

Write tests.

User interface changes

None.

API changes

None.

Data model changes

None.

Issue fork drupal-3054821

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mr.baileys created an issue. See original summary.

wim leers’s picture

Component: cache system » request processing system
Status: Needs review » Needs work
Issue tags: +Needs tests

Thanks for all the research you did here and over at #3016776-24: 301 redirects don't contain cache-control header, @mr.baileys!

This indeed still needs test coverage. AFAICT \Drupal\Tests\page_cache\Functional\PageCacheTest and \Drupal\Tests\system\Functional\Routing\RouterTest::testFinishResponseSubscriber() are the appropriate places to expand test coverage.

mr.baileys’s picture

Status: Needs work » Needs review
StatusFileSize
new2.45 KB
new3.22 KB

Test added to \Drupal\Tests\system\Functional\Routing\RouterTest::testFinishResponseSubscriber() that verifies the cache-control header on redirect responses.

mr.baileys’s picture

Status: Needs review » Needs work

Test-only patch should fail since Drupal currently is not sending the cache-control header for 301 responses, so the test obviously needs work.

mr.baileys’s picture

Assigned: Unassigned » mr.baileys
mr.baileys’s picture

Status: Needs work » Needs review
StatusFileSize
new3.65 KB

Work in progress. The test is still failing, although I can see that the cache-control header *is* initially added in FinishResponseSubscriber. Somehow, between adding it in FRS and receiving the response in PageCacheTest::testCacheableRedirectResponses, the header is removed someplace...

Status: Needs review » Needs work

The last submitted patch, 6: core-301-redirect-cache-control-6.patch, failed testing. View results

mr.baileys’s picture

Assigned: mr.baileys » Unassigned
Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new3.77 KB
new5.22 KB

Finally figured out why manual testing (using the Redirect module) was working, while the automated test failed. The manual test was returning a TrustedRedirectResponse, while my test was running with the CacheableResponseRequest.

Turns out that RedirectResponseSubscriber::checkRedirectUrl() transforms all RedirectResponses that are not an instance of SecuredRedirectResponse to LocalRedirectResponse using LocalRedirectResponse::createFromRedirectResponse(). The latter will indirectly invoke \Symfony\Component\HttpFoundation\RedirectResponse::__construct(), passing url, status code and headers. This constructor will always clear the cache-control header on the newly created response object for 301 responses, unless a "cache-control" header is present in $headers. Unfortunately, if RedirectResponseSubscriber runs after FinishResponseSubscriber (they have the same weight, but in all my testing RRS was fired after FRS), the header is passed in as "Cache-Control", and Symfony uses the case-senstive array_key_exists to check for "cache-control", this wrongly concluding that no cache-control header is present...

One option is to run RedirectResponseSubscriber right before FinishResponseSubscriber. Patch attached that takes this approach.

The last submitted patch, 8: core-301-redirect-cache-control-8-test-only.patch, failed testing. View results

wim leers’s picture

Status: Needs review » Needs work

Symfony uses the case-senstive array_key_exists to check for "cache-control", this wrongly concluding that no cache-control header is present...

Woah, really!? If true, that'd be a bug in Symfony. Symfony stores everything in lowercase (just look at \Symfony\Component\HttpFoundation\HeaderBag::get() and \Symfony\Component\HttpFoundation\HeaderBag::set()), so I'm surprised to hear that. But you're right, I see the case-sensitive check in \Symfony\Component\HttpFoundation\RedirectResponse::__construct(). We should file an upstream bug for that.

That being said, rather than changing the event subscriber priority, I think an alternative solution is to change \Drupal\Component\HttpFoundation\SecuredRedirectResponse::createFromRedirectResponse(), which does:

$safe_response = new static($response->getTargetUrl(), $response->getStatusCode(), $response->headers->allPreserveCase());

Note the ->allPreserveCase()! Changing it to ->all() presumably fixes this edge case too.

+++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
@@ -398,4 +400,18 @@ public function getCacheableResponseWithCustomCacheControl() {
+  public function cacheableRedirectResponse($status_code) {
...
+  public function localRedirectResponse($status_code) {

Nit: I find this naming a bit confusing: it makes it sound like a redirect response is either cacheable or local.

But class LocalRedirectResponse extends CacheableSecuredRedirectResponse: it is cacheable too!

Still, it's worth testing both. But if we're testing both, shouldn't we also test TrustedRedirectResponse?

mr.baileys’s picture

Thanks for the feedback @Wim Leers!

We should file an upstream bug for that.

I'll look into this and open an issue there.

That being said, rather than changing the event subscriber priority, I think an alternative solution is to change \Drupal\Component\HttpFoundation\SecuredRedirectResponse::createFromRedirectResponse(), which does:

$safe_response = new static($response->getTargetUrl(), $response->getStatusCode(), $response->headers->allPreserveCase());

Note the ->allPreserveCase()! Changing it to ->all() presumably fixes this edge case too.

I considered this, but this would mean we lose capitalization on *all* headers once the RedirectResponse gets converted to the SecuredRedirectResponse? Since "FinishResponseSubscriber" implies that it runs at the end of the flow, I figured that having RedirectResponseSubscriber run earlier was a cleaner solution. Happy to switch from ->allPreserveCase() to ->all() if losing capitalisation is acceptable.

wim leers’s picture

Happy to switch from ->allPreserveCase() to ->all() if losing capitalisation is acceptable.

Let's see if it causes test failures. The capitalization of headers isn't meaningful anyway (hence Symfony lowercasing everything!).

Not changing event subscriber priority will make this much easier to land.

mr.baileys’s picture

Status: Needs work » Needs review
StatusFileSize
new1.52 KB
new5.37 KB

Ok, let's try. Patch attached reverts to the current subscriber priorities. Still need to address some of the comments from #10, but want to run this approach through the testbot first.

Status: Needs review » Needs work

The last submitted patch, 13: core-301-redirect-cache-control-13.patch, failed testing. View results

mr.baileys’s picture

Status: Needs work » Needs review
StatusFileSize
new1.27 KB
new6.64 KB
mr.baileys’s picture

StatusFileSize
new7.19 KB
new3.73 KB
+  public function cacheableRedirectResponse($status_code) {
...
+  public function localRedirectResponse($status_code) {

Nit: I find this naming a bit confusing: it makes it sound like a redirect response is either cacheable or local.

But class LocalRedirectResponse extends CacheableSecuredRedirectResponse: it is cacheable too!

Agree, but this is more an issue with how the classes are named right now, might be more confusing if we deviate from that? I did rename the functions in the controller to "respondWith[ClassName]", which is clearer and more in line with existing functions in that class.

Still, it's worth testing both. But if we're testing both, shouldn't we also test TrustedRedirectResponse?

Done!

Status: Needs review » Needs work

The last submitted patch, 16: core-301-redirect-cache-control-16.patch, failed testing. View results

mr.baileys’s picture

Status: Needs work » Needs review

Unrelated test failure, setting back to NR

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
mr.baileys’s picture

I have opened an issue upstream against Symfony 3.4 (https://github.com/symfony/symfony/issues/31862)

wiifm’s picture

I have applied the patch at #16 to a Drupal 8 codebase I help to look after, and have got the latest stable redirect module enabled.

$ curl -sIXGET 'https://www.SITE.com/includes/webtrends.min.js' | grep -iE 'cf-|cache|varnish'
cache-control: public, max-age=2764800
via: varnish
x-cache: HIT
x-cache-hits: 7
cf-cache-status: HIT
cf-ray: 4e2d78bebc51d6a9-SYD

The result is perfect, I am seeing the correct cache-control header being sent, and Varnish and Cloudfare are able to now cache this for 30 days.

Thanks @mr.baileys

RTBC +1

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php
    @@ -27,7 +27,7 @@ abstract class SecuredRedirectResponse extends RedirectResponse {
    -    $safe_response = new static($response->getTargetUrl(), $response->getStatusCode(), $response->headers->allPreserveCase());
    +    $safe_response = new static($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all());
    

    I spent some time thinking about this change. Note the change here is whether or not the header keys are lowercased - the values remain untouched. I believe that the http spec doesn't care if it is Cache-Control or cache-control. As per the spec - https://tools.ietf.org/html/rfc7230#section-3.2 - header field names are case-insensitive

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
    @@ -202,7 +202,7 @@ public function onRespond(FilterResponseEvent $event) {
    -    return $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate';
    +    return !is_null($cache_control) && $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate';
    

    I've ummed and ahhed over this for quite a bit of time. I think this change makes it impossible for use to emit a response without a cache header. Which feels limiting but maybe in a good way. But it is definitely surprising because before if I do what Symfony's RedirectResponse does - ie. $this->headers->remove('cache-control'); - what I expect to happen happens. A response without cache headers is emitted. I think we might only want to change this behaviour if we're dealing with 301s.

    Also regardless of whether we make that change the extensive docs of this method need changing to mention the NULL case.

    I think this code could look like

      protected function isCacheControlCustomized(Response $response) {
        if ($response->getStatusCode() === 301 && !$response->headers->has('Cache-Control')) {
          return FALSE;
        }
        $cache_control = $response->headers->get('Cache-Control');
        return $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate';
      }
    
  3. I spent sometime debating whether or not Drupal should be emitting a cache-control header for 301s. Or whether this should be up to configuring the reverse proxy correctly. Certainly Symfony has made the decision that 301 caching is the responsibility of the layers above the application hence removing the cache-control header. I'm torn on this. Because 301 is a permanent redirect therefore it strictest terms varnish / cloudflare would be well within scope to cache these forever. OTOH the reality is the cloudflare and Acquia take the same approach - https://support.cloudflare.com/hc/en-us/articles/200168326-Are-301-and-3... - that is, short term caching if there is no cache-control header. The default varnish behaviour is odd but then again varnish configuration seems to be aspire to create its own job market with its arcaneness.

    So... I think if we limit the not set cache header behaviour to 301s then this might be a good change because out-of-box 301s cache like any other redirect. Which kinda makes sense.

berdir’s picture

> Because 301 is a permanent redirect therefore it strictest terms varnish / cloudflare would be well within scope to cache these forever.

Hm, if you take redirect.module as an example, you can change those redirects again and on many sites, most redirects are created automatically and it's not uncommon to change it back to not being a redirect, e.g. /article/foobar => article/foobar-update => /article/foobar. Redirect adds cache tags to invalidate them internally, that works fine, but caching them externally is actually somewhat tricky.

alexpott’s picture

https://stackoverflow.com/questions/9130422/how-long-do-browsers-cache-h... so chrome at least will cache the current 301s emitted by core permanently. It feels to me that we should be using 301s very sparingly. Because hint is in the name permanent redirect. In the example in #23 these should not be 301s.

mr.baileys’s picture

Status: Needs work » Needs review
StatusFileSize
new1.15 KB
new7.37 KB

#22.1

I spent some time thinking about this change. Note the change here is whether or not the header keys are lowercased - the values remain untouched.

Correct. The only reason this change is required is the fact that Symfony incorrectly does a case sensitive comparison to check for the presence of the Cache-Control header in \Symfony\Component\HttpFoundation\RedirectResponse::__construct. This has been fixed in Symfony 3.4, but for now we need to work around that bug. This drawback is that capitalization on Drupal 301 responses is not consistent with non-301 responses.

There are two alternatives:

  1. Instead of lowercasing all header names, we could just lowercase the cache-control header prior to calling the constructor;
  2. Have RedirectResponseSubscriber run /before/ FinishResponseSubscriber. (see #3054821-8: Include Cache-Control header on 301 redirects.). This would mean no manipulation is required on the headers since the correct Cache-Control header is not added until after the LocalRedirectResponse is created, but changing the event subscriber order might have more far-reaching effects.

#22.2

+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -202,7 +202,7 @@ public function onRespond(FilterResponseEvent $event) {
- return $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate';
+ return !is_null($cache_control) && $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate';

I've ummed and ahhed over this for quite a bit of time. I think this change makes it impossible for use to emit a response without a cache header. Which feels limiting but maybe in a good way. But it is definitely surprising because before if I do what Symfony's RedirectResponse does - ie. $this->headers->remove('cache-control'); - what I expect to happen happens. A response without cache headers is emitted. I think we might only want to change this behaviour if we're dealing with 301s.

Adjusted so that the changes to isCacheControlCustomized() are limited to 301 responses.

#22.3

IMO, the fact that currently the time-frame for which a 301 is cached is unpredictable (depends on the strategy employed by intermediate layers and browsers) makes a strong case for explicitly setting the header in Drupal, as this makes the behaviour predictable (and somewhat configurable through the page cache max age setting.)

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

pobster’s picture

StatusFileSize
new8.21 KB

Just a reroll for 8.8.1 (latest 8.8.x anyway...)

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

neclimdul’s picture

This is a weird issue. Had to read it half a dozen times to really grok the interactions.

  1. +++ b/core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php
    @@ -27,7 +27,7 @@ abstract class SecuredRedirectResponse extends RedirectResponse {
    -    $safe_response = new static($response->getTargetUrl(), $response->getStatusCode(), $response->headers->allPreserveCase());
    +    $safe_response = new static($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all());
    
    This has been fixed in Symfony 3.4, but for now we need to work around that bug.

    This has sat around long enough its going to land in 9.x which means will be requiring 4.x+. Reading back through the comments and the Symfony issue it sounds that means this chunk isn't required anymore.

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
    @@ -201,6 +201,13 @@ public function onRespond(FilterResponseEvent $event) {
    +    // Symfony explicitly removes the Cache-Control header for 301 redirects
    +    // which do not have a custom Cache-Control header. Treat those redirect
    +    // responses as not customized.
    +    if ($response->getStatusCode() === 301 && !$response->headers->has('Cache-Control')) {
    +      return FALSE;
    +    }
    +
    

    Man their logic really puts us in a sticky situation here. This docblock is a bit confusing. I tried to come up with a way of explaining how but I'm struggling. Maybe we can just add @see Symfony\Component\HttpFoundation\RedirectResponse::__construct so the next person struggling with this can follow where the logic is we're matching.

    Corollary, if I make a Response with a 301 status code this is going to match I think. Is that ok? I don't have the answer to that.

  3. +++ b/core/tests/Drupal/Tests/Component/HttpFoundation/SecuredRedirectResponseTest.php
    @@ -37,11 +37,8 @@ public function testRedirectCopy() {
    -    // We unset cache headers so we don't test arcane Symfony weirdness.
    -    // https://github.com/symfony/symfony/issues/16171
    -    unset($headers1['Cache-Control'], $headers2['Cache-Control']);
    

    That's weird... who wrote that? Wait what? Oh, hey look over there at that other thing that's not a hacky test.

    Glad we're on versions where that hack is not needed. Sorry I didn't follow up.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

emanuelrighetto’s picture

This patch solved a rather annoying problem on my Drupal 9.3.x-dev installation.
Let me explain the characteristics of what I think is a bug: without the patch the redirection (only for anonimous user) from https://mydomain.ext/node/6 to the specifict node 6 path alias raised an exception:

The website encountered an unexpected error. Please try again later.
TypeError: Argument 1 passed to str_contains() must be of the type string, null given, called in /var/www/vhosts/scarpepervocazione.it/httpdocs/vendor/symfony/http-foundation/Response.php on line 314 in str_contains() (line 29 of /var/www/vhosts/scarpepervocazione.it/httpdocs/vendor/symfony/polyfill-php80/bootstrap.php).

str_contains() (Line: 314)
Symfony\Component\HttpFoundation\Response->prepare() (Line: 720)
Drupal\Core\DrupalKernel->handle() (Line: 19)

On line 314 of vendor/symfony/http-foundation/Response.php there is this check:

        // Check if we need to send extra expire info headers
        if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control'), 'no-cache')) {
            $headers->set('pragma', 'no-cache');
            $headers->set('expires', -1);
        }

I have only noticed this behaviour for anonymous users; if the user is logged in, the redirection from https://mydomain.ext/node/6 to https://mydomain.ext/page/this-is-a-sample-path-alias is completed without error.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new168 bytes

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.

anybody’s picture

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 #3311406: .htaccess ExpiresDefault (2W) is much too low. Should be ~1Y?

bkosborne’s picture

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.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

sime’s picture

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

sime’s picture

StatusFileSize
new7.65 KB

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

sime’s picture

sime’s picture

StatusFileSize
new7.53 KB

Lint, passing tests

sime’s picture

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

pameeela’s picture

Status: Needs work » Needs review

Patch applies still, setting to NR.

smustgrave’s picture

Status: Needs review » Needs work

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

Converting to an MR is also preferred if possibe

angrytoast made their first commit to this issue’s fork.

angrytoast’s picture

Status: Needs work » Needs review

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.

mr.baileys’s picture

smustgrave’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
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.

quietone’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

This needs a rebase.

pobster’s picture

It needs a bit of rewriting now too, as we have AssertPageCacheContextsAndTagsTrait for enabling page cache and checking header(s). Might as well use it.

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

tim@lammar.be’s picture

StatusFileSize
new7.65 KB

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

tim@lammar.be’s picture

StatusFileSize
new7.56 KB

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

pobster’s picture

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);
    }
  }
}
longwave’s picture

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

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

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

richardgaunt’s picture

Patches rolled into the MR by Sean, hiding patches.

richardgaunt’s picture

Issue tags: +DrupalSouth
quietone’s picture

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.

wiifm’s picture

wiifm’s picture

Status: Needs work » Needs review
wiifm’s picture

Issue tags: +DrupalSouth 2024
acbramley’s picture

Adding credit for contributors during the DS2024 Code sprint.

Just 1 final question left on the MR.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs Review Queue Initiative

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

wim leers’s picture

Issue tags: +scalability

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

alexpott’s picture

Version: 11.x-dev » 10.3.x-dev
Status: Reviewed & tested by the community » Fixed

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

  • alexpott committed ec45fd56 on 10.3.x
    Issue #3054821 by mr.baileys, wiifm, sime, angrytoast, pobster,...

  • alexpott committed 2fbe22aa on 11.x
    Issue #3054821 by mr.baileys, wiifm, sime, angrytoast, pobster,...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.