[PATCH] Upstream: prioritise Cache-Control over Expires
Maxim Dounin
mdounin at mdounin.ru
Sat Jun 11 22:52:50 UTC 2022
Hello!
On Fri, Jun 10, 2022 at 03:37:34PM +0400, Sergey Kandaurov wrote:
> > On 8 Jun 2022, at 04:57, Maxim Dounin <mdounin at mdounin.ru> wrote:
> >
> > On Tue, Jun 07, 2022 at 07:58:36PM +0400, Sergey Kandaurov wrote:
[...]
> > Also, it might be a good idea to make sure that various
> > other factors to disable cache, such as Set-Cookie, are not
> > ignored when a resource is returned with Expires in the past and
> > valid Cache-Control max-age. There were multiple attempts to
> > submit patches which failed to handle this properly. E.g.:
> >
> > location /set-cookie {
> > add_header Set-Cookie foo;
> > add_header Cache-Control max-age=60;
> > return 204;
> > }
>
> If I got you right, it refers to earlier patches in January
> with erroneously missed Set-Cookie handling.
> I reproduced this with Expires in the past and X-Accel-Expires:
>
> location /set-cookie {
> add_header Set-Cookie foo;
> add_header Expires "Thu, 01 Jan 1970 00:00:01 GMT";
> add_header X-Accel-Expires 60;
> return 204;
> }
I believe I've seen more than one patch which missed Set-Cookie
(and Vary) handling and tried to simply set cacheable back to 1.
Using "Cache-Control: max-age=60" might be better, as RFC
explicitly says to ignore Expires if "Cache-Control: max-age" is
present, and does not define X-Accel-Expires handling.
While it is highly unlikely that at some point we'll decide that
X-Accel-Expires should be used regardless of other factors, such
as Set-Cookie and Vary, I don't see specific reasons to state the
opposite in the tests.
[...]
> >> +my $time = time() - 1;
> >> +like(get("/rev?c=max-age=60,stale-while-revalidate=60&x=\@$time"),
> >> + qr/STALE/, 'Cache-Control extensions after X-Accel-Expires');
> >
> > Something like:
> >
> > X-Accel-Expires: @1
> > Cache-Control: stale-while-revalidate=2145902155
> >
> > shouldn't require any calculations (and can be placed in an
> > explicit location). And the opposite order probably worth
> > checking too.
>
> While it's better to avoid math, this one is somewhat synthetic.
> Though, I have no objection in principle, good enough for tests.
>
> >
> > Not sure if "max-age=60" is at all needed here, though probably
> > it can catch some potential bugs.
> >
>
> Since "stale-while-revalidate" is an extension, I assume that
> it should be given with some base parameter, such as "max-age".
> The assumption is supported by examples in RFC 5861.
The "stale-while-revalidate" is an extension as it extends the
Cache-Control header with an additional directive. Freshness
lifetime might be determined by other means, not just by max-age
directive in the same header field, see RFC 9111, "4.2.1.
Calculating Freshness Lifetime"
(https://datatracker.ietf.org/doc/html/rfc9111#section-4.2.1).
In this particular case, X-Accel-Expires is certainly enough to
provide freshness lifetime. In general, I see no reasons why even
a heuristic freshness lifetime wouldn't be good enough.
> This brought me a thought that we don't respect Cache-Control
> preference over Expires if given with only extensions:
>
> Expires: Thu, 31 Dec 2037 23:55:55 GMT
> Cache-Control: stale-while-revalidate=1
>
> In the above, Cache-Control doesn't set/override valid_sec,
> and as such, it is set with Expires on its own.
That's the expected behaviour. With the current code, Expires is
equivalent to the max-age time as set by "Cache-Control: max-age".
This is in line with what RFC recommends, see the link above.
Further, section "5.3. Expires" explicitly talks about
"Cache-Control: max-age", not just Cache-Control
(https://datatracker.ietf.org/doc/html/rfc9111#section-5.3).
> # HG changeset patch
> # User Sergey Kandaurov <pluknet at nginx.com>
> # Date 1654860778 -14400
> # Fri Jun 10 15:32:58 2022 +0400
> # Node ID f8e5ab69e6edd379b2ea9e0e8f1ebbab01fd9075
> # Parent 4f238efded81e00c635c146df6ecb585fa0907ec
> Tests: added proxy cache tests with upstream cache headers.
>
> diff --git a/proxy_cache_control.t b/proxy_cache_control.t
> new file mode 100644
> --- /dev/null
> +++ b/proxy_cache_control.t
> @@ -0,0 +1,276 @@
> +#!/usr/bin/perl
> +
> +# (C) Sergey Kandaurov
> +# (C) Nginx, Inc.
> +
> +# Tests for proxy Expires / Cache-Control / X-Accel-Expires.
> +
> +###############################################################################
> +
> +use warnings;
> +use strict;
> +
> +use Test::More;
> +
> +BEGIN { use FindBin; chdir($FindBin::Bin); }
> +
> +use lib 'lib';
> +use Test::Nginx;
> +
> +###############################################################################
> +
> +select STDERR; $| = 1;
> +select STDOUT; $| = 1;
> +
> +my $t = Test::Nginx->new()->has(qw/http proxy cache rewrite/)->plan(19);
> +
> +$t->write_file_expand('nginx.conf', <<'EOF');
> +
> +%%TEST_GLOBALS%%
> +
> +daemon off;
> +
> +events {
> +}
> +
> +http {
> + %%TEST_GLOBALS_HTTP%%
> +
> + proxy_cache_path %%TESTDIR%%/cache keys_zone=NAME:1m;
> +
> + server {
> + listen 127.0.0.1:8080;
> + server_name localhost;
> +
> + add_header X-Cache-Status $upstream_cache_status;
> +
> + location / {
> + proxy_pass http://127.0.0.1:8081;
> + proxy_cache NAME;
> +
> + proxy_cache_background_update on;
> + }
> +
> + location /ignore {
> + proxy_pass http://127.0.0.1:8081;
> + proxy_cache NAME;
> +
> + proxy_ignore_headers Cache-Control Expires;
> + proxy_ignore_headers X-Accel-Expires;
> + }
> + }
> +
> + server {
> + listen 127.0.0.1:8081;
> + server_name localhost;
> +
> + location /expires {
> + add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
> + return 204;
> + }
> +
> + location /cache-control {
> + add_header Cache-Control max-age=60;
> + return 204;
I tend to think that it would be better to remove extra spaces.
It looks weird, especially combined with "return 204;" without
extra spaces.
> + }
> +
> + location /x-accel-expires {
> + add_header X-Accel-Expires 60;
> + return 204;
> + }
> +
> + location /x-accel-expires-at {
> + add_header X-Accel-Expires @60;
> + return 204;
> + }
> +
> + location /x-accel-expires-duplicate {
> + add_header X-Accel-Expires 60;
> + add_header X-Accel-Expires 0;
> + return 204;
> + }
> +
> + location /ignore {
> + add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
> + add_header Cache-Control max-age=60;
> + add_header X-Accel-Expires 60;
> + return 204;
> + }
> +
> + location /cache-control-after-expires {
> + add_header Expires "Thu, 01 Jan 1970 00:00:01 GMT";
> + add_header Cache-Control max-age=60;
> + return 204;
> + }
> +
> + location /cache-control-before-expires {
> + add_header Cache-Control max-age=60;
> + add_header Expires "Thu, 01 Jan 1970 00:00:01 GMT";
> + return 204;
> + }
See below about before/after order.
> +
> + location /cache-control-no-cache-after-expires {
> + add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
> + add_header Cache-Control no-cache;
> + return 204;
> + }
> +
> + location /cache-control-no-cache-before-expires {
> + add_header Cache-Control no-cache;
> + add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
> + return 204;
> + }
> +
> + location /x-accel-expires-after {
> + add_header Expires "Thu, 01 Jan 1970 00:00:01 GMT";
> + add_header Cache-Control no-cache;
> + add_header X-Accel-Expires 60;
> + return 204;
> + }
> +
> + location /x-accel-expires-before {
> + add_header X-Accel-Expires 60;
> + add_header Expires "Thu, 01 Jan 1970 00:00:01 GMT";
> + add_header Cache-Control no-cache;
> + return 204;
> + }
> +
> + location /x-accel-expires-0-after {
> + add_header Cache-Control max-age=60;
> + add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
> + add_header X-Accel-Expires 0;
> + return 204;
> + }
> +
> + location /x-accel-expires-0-before {
> + add_header X-Accel-Expires 0;
> + add_header Cache-Control max-age=60;
> + add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
> + return 204;
> + }
> +
> + location /cache-control-no-cache-one {
> + add_header Cache-Control "no-cache, max-age=60";
> + return 204;
> + }
> +
> + location /cache-control-no-cache-multi {
> + add_header Cache-Control no-cache;
> + add_header Cache-Control max-age=60;
> + return 204;
> + }
> +
> + location /extension-before-x-accel-expires {
> + add_header Cache-Control
> + "max-age=60, stale-while-revalidate=2145902155";
> + add_header X-Accel-Expires @1;
> + return 204;
See above about "max-age=".
> + }
> +
> + location /extension-after-x-accel-expires {
> + add_header X-Accel-Expires @1;
> + add_header Cache-Control
> + "max-age=60, stale-while-revalidate=2145902155";
> + return 204;
> + }
> +
> + location /set-cookie {
> + add_header Set-Cookie foo;
> + add_header Expires "Thu, 01 Jan 1970 00:00:01 GMT";
> + add_header X-Accel-Expires 60;
> + return 204;
> + }
> + }
> +}
> +
> +EOF
> +
> +$t->run();
> +
> +###############################################################################
> +
> +# upstream cache headers work
> +
> +like(get('/expires'), qr/HIT/, 'expires');
> +like(get('/cache-control'), qr/HIT/, 'cache-control');
> +like(get('/x-accel-expires'), qr/HIT/, 'x-accel-expires');
> +like(get('/x-accel-expires-at'), qr/EXPIRED/, 'x-accel-expires at');
> +
> +TODO: {
> +local $TODO = 'not yet' unless $t->has_version('1.23.0');
> +
> +# the second header to disable cache is duplicate and ignored
> +
> +like(get('/x-accel-expires-duplicate'), qr/HIT/, 'x-accel-expires duplicate');
> +
> +}
> +
> +# with cache headers ignored, the response will be fresh
> +
> +like(get('/ignore'), qr/MISS/, 'cache headers ignored');
> +
> +# Cache-Control is preferred over Expires
> +
> +TODO: {
> +local $TODO = 'not yet' unless $t->has_version('1.23.0');
> +
> +like(get('/cache-control-after-expires'), qr/HIT/,
> + 'cache-control after expires');
> +
> +}
> +
> +like(get('/cache-control-before-expires'), qr/HIT/,
> + 'cache-control before expires');
It might make sense to use the opposite order for before/after, to
make sure TODO tests are after corresponding tests which pass in
the previous versions. YMMV though.
> +
> +like(get('/cache-control-no-cache-after-expires'), qr/MISS/,
> + 'cache-control no-cache after expires');
> +like(get('/cache-control-no-cache-before-expires'), qr/MISS/,
> + 'cache-control no-cache before expires');
> +
> +# X-Accel-Expires is preferred over both Cache-Control and Expires
> +
> +TODO: {
> +local $TODO = 'not yet' unless $t->has_version('1.23.0');
> +
> +like(get('/x-accel-expires-after'), qr/HIT/, 'x-accel-expires after');
> +
> +}
> +
> +like(get('/x-accel-expires-before'), qr/HIT/, 'x-accel-expires before');
> +
> +like(get('/x-accel-expires-0-after'), qr/MISS/, 'x-accel-expires 0 after');
> +like(get('/x-accel-expires-0-before'), qr/MISS/, 'x-accel-expires 0 before');
> +
> +# "Cache-Control: no-cache" disables caching, no matter of "max-age".
Style nit: extra dot.
> +
> +like(get('/cache-control-no-cache-one'), qr/MISS/,
> + 'cache-control no-cache');
> +like(get('/cache-control-no-cache-multi'), qr/MISS/,
> + 'cache-control no-cache multi line');
> +
> +# Set-Cookie is considered when caching with X-Accel-Expires
> +
> +like(get('/set-cookie'), qr/MISS/, 'set-cookie not cached');
> +
> +# Cache-Control extensions are preserved with X-Accel-Expires
> +
> +like(get('/extension-before-x-accel-expires'),
> + qr/STALE/, 'cache-control extensions before x-accel-expires');
> +
> +TODO: {
> +local $TODO = 'not yet' unless $t->has_version('1.23.0');
> +
> +like(get('/extension-after-x-accel-expires'),
> + qr/STALE/, 'cache-control extensions after x-accel-expires');
> +
> +}
> +
> +###############################################################################
> +
> +sub get {
> + my ($uri) = @_;
> + http_get($uri);
> + http_get($uri);
> +}
> +
> +###############################################################################
Overall looks good, see minor comments above.
--
Maxim Dounin
http://mdounin.ru/
More information about the nginx-devel
mailing list