Updated: Comment #N
Problem/Motivation
When the page cache is active, accessing a private file leads to a 500 response. The file content itself is still accessible though. The problem is likely that FinishResponseSubscriber is executed for all responses (including private file downloads) while back in Drupal 7 comparable drupal_page_footer was not because private file downloads triggered an early exit (see file_transfer).
As a consequence the page cache is active for private file downloads in Drupal 8 while it was not on D7 and therefor we need to disable it explicitely on private file downloads.
Error log
Uncaught PHP Exception LogicException: "The content cannot be set on a BinaryFileResponse instance." at /tmp/drupal/core/vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/BinaryFileResponse.php line 272
#0 Symfony\Component\HttpFoundation\BinaryFileResponse->setContent() called at [core/includes/bootstrap.inc:960]
#1 drupal_serve_page_from_cache() called at [core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php:105]
#2 Drupal\Core\EventSubscriber\FinishResponseSubscriber->onRespond()
#3 call_user_func() called at [core/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php:164]
#4 Symfony\Component\EventDispatcher\EventDispatcher->doDispatch() called at [core/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php:53]
#5 Symfony\Component\EventDispatcher\EventDispatcher->dispatch() called at [core/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php:167]
#6 Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch() called at [core/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/HttpKernel.php:166]
#7 Symfony\Component\HttpKernel\HttpKernel->filterResponse() called at [core/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/HttpKernel.php:148]
#8 Symfony\Component\HttpKernel\HttpKernel->handleRaw() called at [core/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/HttpKernel.php:66]
#9 Symfony\Component\HttpKernel\HttpKernel->handle() called at [core/lib/Drupal/Core/HttpKernel.php:65]
#10 Drupal\Core\HttpKernel->handle() called at [core/lib/Drupal/Core/DrupalKernel.php:284]
#11 Drupal\Core\DrupalKernel->handle() called at [core/includes/bootstrap.inc:1478]
#12 drupal_handle_request() called at [index.php:15]
HTTP Request/Response:
Request URL:http://localhost:3000/system/files/private.txt
Request Method:GET
Status Code:500 Internal Server Error
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip,deflate,sdch
Accept-Language:de,en-US;q=0.8,en;q=0.6,fr;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Host:localhost:3000
Referer:http://localhost:3000/node/1
Response Headers
Cache-Control:must-revalidate, no-cache, post-check=0, pre-check=0, private
Connection:close
content-language:en
content-language:en
content-length:3467
Content-Type:text/plain; charset=UTF-8
date:Sat, 15 Mar 2014 15:17:34 GMT
date:Sat, 15 Mar 2014 15:17:34 GMT
etag:"1394896654"
etag:"1394896654"
Expires:Sun, 19 Nov 1978 05:00:00 GMT
last-modified:Sat, 15 Mar 2014 15:17:34 GMT
last-modified:Sat, 15 Mar 2014 15:17:34 GMT
Server:lighttpd/1.4.31
x-drupal-cache:MISS
x-generator:Drupal 8 (http://drupal.org)
X-Powered-By:PHP/5.4.26-1~dotdeb.1
x-ua-compatible:IE=edge,chrome=1
x-ua-compatible:IE=edge,chrome=1
Proposed resolution
- Add tests for private file downloads and private image style derivative when page cache is active. Also port them to D7 after they are committed to D8.
- Choose between two alternative approaches:
- Either disable the page cache selectively from within
FileDownloadController and ImageStyleDownloadController (patch #7)
- Or prevent caching of
BinaryFileResponse altogether (patch #8)
Remaining tasks
User interface changes
None
API changes
None
Comments
Comment #1
znerol commentedA patch exposing the failure condition. I noticed that
Drupal\file\Tests\DownloadTestis frighteningly small. Are there more tests in some other place covering private file downloads?Comment #2
znerol commentedTrigger the test-bot.
Comment #3
znerol commentedAdding a
drupal_page_is_cacheable(FALSE);toFileDownloadController::downloadseems to cut it. However it is necessary to fix the heavy-handed x-drupal-cache-header hack before the test will actually pass. That makes this patch bigger than it should be.Let's see what other things break...
Comment #6
znerol commented3: 2218463-fix-private-file-download-when-page-cache-is-active.diff queued for re-testing.
Comment #7
znerol commentedSame for private images when an image derivative is rendered for the first time. I'm wondering whether it would be better to just disable the page cache for
BinaryFileResponse.Comment #8
znerol commentedThis is the alternative approach: Just filter out
BinaryFileResponse. I feel that this approach is much more future proved than trying to disable the page cache selectively on all download-controllers. Tests are identical to the ones in #7.Comment #9
znerol commentedComment #10
znerol commentedComment #12
rbayliss commentedThis is also true of image style downloads, the first time they are generated. I was really hoping there was a better way to fix this than to explicitly check for certain response types before serving from the cache, but for right now, it looks like there isn't. See https://github.com/symfony/symfony/issues/9128
Comment #13
rbayliss commentedRerolled and added an exclusion for StreamedResponse as per the Symfony issue linked above. I also moved the exclusions into drupal_set_page_cache, since that's where the determination of the page's cacheability is currently happening (drupal_page_is_cacheable()). Last but not least, I removed the previous changes to the x-drupal-cache headers, which didn't seem to be needed. Let's see if this passes.
Comment #15
znerol commentedWe are testing for the presence of
X-Drupal-Cacheheader in some tests in order to tell whether the page cache was active during generation of the response. When it is not acceptable to get rid of the header-hack in this issue, it would be possible to remove theX-Drupal-Cacheheader from withindrupal_page_set_cacheforBinaryFileResponseandStreamedResponse. Note that currently the header is only removed from the cache-object and not from the response itself.Comment #16
rbayliss commentedRerolled based on znerol's patch in comment 8 (please ignore the one I submitted in between), and just added the StreamedResponse as I mentioned in comment 13. Interdiff is against the patch in comment 8.
Comment #17
znerol commentedMaking this a sub-issue of #2257695: [meta] Modernize the page cache
Comment #18
znerol commentedReroll after #2257709: Remove the interdependence between the internal page cache and management of the Cache-Control header for external caches which removed the x-drupal-cache header hack. Sorry, no proper interdiff, but the changes to non-test-code are now down to one line (
FinishResponseSubscriber).Comment #21
znerol commentedForgot the use-statements...
Comment #22
znerol commentedComment #23
dawehnerSadly there is no interface or method we can call but indeed have to hardcode it here. It would be great to explain why we do this here.
I wonder whether we could name the first one like "testPrivteFileTransferWithoutPageCache"
Comment #24
znerol commentedComment #25
damiankloip commentedHere is a reroll for psr-4. I also tested this manually as I just hit the same issue. I think this patch looks good.
Comment #26
damiankloip commented25: 2218463-25.patch queued for re-testing.
Comment #28
damiankloip commentedReroll.
Comment #29
berdirComment makes sense, tests fail without the fix, let's get this in.
Comment #30
wim leersThe CDN module in Drupal 7 has a "Far Future expiration" setting. In short, it serves a file with optimal caching headers. Including binary files.
I agree with this:
But AFAICT this patch affects *all* binary responses, not just those for private files and image styles?
I think that the overarching problem is this:
$is_cacheablecontrols both whether caching headers will be set and whether the response is going to be stored in the page cache. It should be possible to have caching headers without storing the response in the page cache.Comment #31
damiankloip commentedHmm, that's a very good point. So we need to do something like this instead?
EDIT: Quick patch disclaimer.
Comment #33
znerol commentedI feel that that restriction can be attributed to the backend. Therefore let's move the check to
drupal_page_set_cache.Tests expect that derived images should not be cacheable at all upon the first retrieval (i.e. when they were generated by
ImageStyleDownloadController::deliver). Is this really what we want? In contrast private file downloads setCache-Control: privateallowing the browser to cache the response (seefile_get_content_headers).Interdiff is against #28.
Comment #34
damiankloip commentedNot sure about using 'recorded'. Saved, stored, set.. or something.
It is not the best thing relying on the return value of a function known for just setting. Works for me though.
Although this does not really just seem like a restriction from the backend. This is more a flaw/shortcoming in how Symfony deals with binary and streamed responses? Also looks like this is not likely to change at any point soon.
Comment #35
znerol commentedOk, then let's go with #31 plus the fix from
ImageStyleDownloadController.php.Comment #36
znerol commentedLet's not reintroduce the dependency of the internal cache to the external configuration like #31 does and just fix the 500 error.
Interdiff is against #28.
Comment #37
wim leersThat looks much better, thank you! :)
Comment #38
damiankloip commentedYep +1 on the latest patch. Includes everything we have discussed from #30 onwards.
Comment #39
wim leersComment #40
alexpottCommitted 0dc696d and pushed to 8.x. Thanks!