Welcome! Log In Create A New Profile

Advanced

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin
April 24, 2022 12:56AM
Hello!

On Fri, Apr 22, 2022 at 08:06:08PM +0100, Vadim Fedorenko via nginx-devel wrote:

> Hi Maxim!
>
> Thanks for the feedback. As you might have already seen, I sent another version
> of this patch which addresses some of style and readability issues as well as
> X-Accel-Expires issue in v3.

Ah, sorry, I've missed that you've removed the
"u->headers_in.x_accel_expires != NULL" check in the
ngx_http_upstream_process_cache_control(). So the
stale-while-revalidate and stale-if-error Cache-Control directive
are always applied now, whenever X-Accel-Expires is present or
not.

Not sure this is a good approach: simply ignoring Cache-Control if
X-Accel-Expires is present might be a better and easier to
understand solution.

The basic idea behind X-Accel-Expires is that site administrators
might want to provide distinct caching parameters to caches they
control (and can purge or force-update when needed), and these
caching parameters are different from caching parameters sent to
external caches. Still using some caching parameters from
Cache-Control does not seem to align well with this usage.

On the other hand, as per RFC 2616/7234, the Expires header is
only expected to be ignored when "Cache-Control: max-age" is
present, but not other Cache-Control directives. So probably you
are right and this is correct approach to use.

> Regarding RFC issue. RFC 7234 Section 5.1 explicitly states that Expires MUST be
> ignored if reponse contains Cache-Control header with max-age or s-maxage
> directives. That's why I said that Nginx server configured to cache responses
> and to rely on headers to determine the freshness violates RFC. And this is true
> for a subset of absolutely legitimate responses, and it was done intended to
> actually optimize the internal processing but not because of configuration
> optimizations.

Sure. Though it is also completely legitimate to don't cache a
response, regardless of the particular headers. Switching of
caching for any reason is generally safe, and makes further RFC
7234 requirements void.

Rather, I would say that nginx violates RFC by caching a response
based on "Cache-Control: max-age" when there is an Expires header
which prevents caching, as nginx does not support the Age header
and therefore can incorrectly cache an expired response.

> Hope you will have some time to review version 3 of my patch even though it
> might not clearly apply on top of the patchset you sent couple of days ago.

Unfortunately, style and readability issues are still there.
Review below.

(Note that it might be a good idea to keep patches in a single
thread. When sending with "hg email", the "--in-reply-to" option
ensures proper threading.)

: # HG changeset patch
: # User Vadim Fedorenko <vadim.fedorenko@cdnnow.ru>
: # Date 1650406016 -10800
: # Wed Apr 20 01:06:56 2022 +0300
: # Node ID e04dac22328020cf8d8abcc4863b982b513b0c80
: # Parent a736a7a613ea6e182ff86fbadcb98bb0f8891c0b
: Upstream: prioritise X-Accel-Expire over Cache-Control and Expire

Note missing "s" in "Expires".
Style: should be a trailing dot.

: RFC7234 explicitly says that cache recipient MUST ignore Expires
: header if response includes Cache-Control header with max-age or
: s-maxage directives. Previously Cache-Control was ignored if it
: was after Expires in reply headers.

Note that this is an inaccurate and misleading statement:
Cache-Control wasn't ignored. The only specific case to address
is when the Expires header comes first and disables caching.

: At the same time documentation states that there is special header
: X-Accel-Expires that controls the behavior of nginx and should be
: prioritized over other cache-specific headers and this patch
: implements this priority.

Again, the X-Accel-Expires header is already prioritized over any
other cache control headers.

: More informantion could be found in ticket #964.

Typo: "information".

References in the "(ticket #964)" form are automatically linked in
Trac and generally recommended unless there are specific reasons
to avoid linking.

: ---
: src/http/ngx_http_upstream.c | 62 +++++++++++++++++++++++++-----------
: src/http/ngx_http_upstream.h | 6 ++++
: 2 files changed, 50 insertions(+), 18 deletions(-)
:
: diff -r a736a7a613ea -r e04dac223280 src/http/ngx_http_upstream.c
: --- a/src/http/ngx_http_upstream.c Tue Feb 08 17:35:27 2022 +0300
: +++ b/src/http/ngx_http_upstream.c Wed Apr 20 01:06:56 2022 +0300
: @@ -2350,6 +2350,32 @@
:
:
: static void
: +ngx_http_upstream_validate_cache_headers(ngx_http_request_t *r, ngx_http_upstream_t *u)
: +{
: + ngx_http_upstream_headers_in_t *uh = &u->headers_in;
: + if (uh->x_accel_expires != NULL &&
: + !(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_XA_EXPIRES)) {

Style issues: more than 80 characters in a line, no empty line
between variable definition and code, less than two spaces between
type and variable name, variable initialization should be on a
separate line, two spaces before "&&", operator should be on the
continuation line, "{" should be on its own line, no function
declaration, function definition should follow first usage unless
there are specific reasons to place it elsewhere.

Also, we don't generally use ngx_http_upstream_headers_in_t local
variables, but rather use direct references via u->headers_in.

: + u->cacheable = uh->x_accel_expires_c;
: + r->cache->valid_sec = uh->x_accel_expires_n;

This uses r->cache without appropriate conditional compilation
directives, so will result in compilation errors without HTTP
cache compiled in ("./configure --without-http-cache").

: + return;
: + }
: +
: + if (uh->cache_control.elts != NULL &&
: + !(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_CACHE_CONTROL)) {
: + u->cacheable = uh->cache_control_c;
: + r->cache->valid_sec = uh->cache_control_n;

This seems to be incorrect, given that something like

Cache-Control: public
Expires: <date>

is exactly equivalent to just "Expires: <date>", without
Cache-Control at all.

: + return;
: + }
: +
: + if (uh->expires != NULL &&
: + !(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_EXPIRES)) {
: + u->cacheable = uh->expires_c;
: + r->cache->valid_sec = uh->expires_n;
: + }
: +}
: +
: +
: +static void
: ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u)
: {
: ssize_t n;
: @@ -2468,6 +2494,11 @@
:
: continue;
: }
: +#if (NGX_HTTP_CACHE)

Conditional compilation here would result in a function which is
never used without HTTP cache compiled in, and hence "unused
function" warnings.

: + if (u->cacheable) {
: + ngx_http_upstream_validate_cache_headers(r, u);
: + }
: +#endif

Caching also needs to be properly adjusted somewhere in
ngx_http_upstream_intercept_errors(), as it does not call
ngx_http_upstream_process_headers().

:
: break;
: }
: @@ -4735,10 +4766,6 @@
: return NGX_OK;
: }
:
: - if (r->cache->valid_sec != 0 && u->headers_in.x_accel_expires != NULL) {
: - return NGX_OK;
: - }
: -
: start = h->value.data;
: last = start + h->value.len;
:
: @@ -4746,7 +4773,7 @@
: || ngx_strlcasestrn(start, last, (u_char *) "no-store", 8 - 1) != NULL
: || ngx_strlcasestrn(start, last, (u_char *) "private", 7 - 1) != NULL)
: {
: - u->cacheable = 0;
: + u->headers_in.cache_control_c = 0;
: return NGX_OK;
: }
:
: @@ -4771,16 +4798,15 @@
: continue;
: }
:
: - u->cacheable = 0;
: return NGX_OK;
: }
:
: if (n == 0) {
: - u->cacheable = 0;
: return NGX_OK;
: }
:
: - r->cache->valid_sec = ngx_time() + n;
: + u->headers_in.cache_control_c = 1;
: + u->headers_in.cache_control_n = ngx_time() + n;
: }
:
: p = ngx_strlcasestrn(start, last, (u_char *) "stale-while-revalidate=",
: @@ -4856,18 +4882,18 @@
: return NGX_OK;
: }
:
: - if (r->cache->valid_sec != 0) {
: + expires = ngx_parse_http_time(h->value.data, h->value.len);
: +
: + if (expires == NGX_ERROR || expires < ngx_time()) {
: return NGX_OK;
: }
:
: - expires = ngx_parse_http_time(h->value.data, h->value.len);
: -
: - if (expires == NGX_ERROR || expires < ngx_time()) {
: - u->cacheable = 0;
: - return NGX_OK;
: - }
: -
: - r->cache->valid_sec = expires;
: + if (u->headers_in.expires_c) {
: + expires = ngx_min(expires, u->headers_in.expires_n);
: + }

The patch series recently posted will take care of duplicate
headers, there is no need to introduce additional ad-hoc
solutions.

: + u->headers_in.expires_n = expires;
: + u->headers_in.expires_c = 1;
: +
: }
: #endif
:
: @@ -4906,14 +4932,12 @@
:
: switch (n) {
: case 0:
: - u->cacheable = 0;
: - /* fall through */
: -
: case NGX_ERROR:
: return NGX_OK;
:
: default:
: - r->cache->valid_sec = ngx_time() + n;
: + u->headers_in.x_accel_expires_c = 1;
: + u->headers_in.x_accel_expires_n = ngx_time() + n;
: return NGX_OK;
: }
: }
: @@ -4924,7 +4948,8 @@
: n = ngx_atoi(p, len);
:
: if (n != NGX_ERROR) {
: - r->cache->valid_sec = n;
: + u->headers_in.x_accel_expires_c = 1;
: + u->headers_in.x_accel_expires_n = ngx_time() + n;
: }
: }
: #endif
: diff -r a736a7a613ea -r e04dac223280 src/http/ngx_http_upstream.h
: --- a/src/http/ngx_http_upstream.h Tue Feb 08 17:35:27 2022 +0300
: +++ b/src/http/ngx_http_upstream.h Wed Apr 20 01:06:56 2022 +0300
: @@ -294,9 +294,15 @@
:
: off_t content_length_n;
: time_t last_modified_time;
: + ngx_int_t cache_control_n;
: + ngx_int_t expires_n;
: + ngx_int_t x_accel_expires_n;
:
: unsigned connection_close:1;
: unsigned chunked:1;
: + unsigned cache_control_c;
: + unsigned expires_c;
: + unsigned x_accel_expires_c;
: } ngx_http_upstream_headers_in_t;
:
:

As far as I can tell, proper behaviour, assuming we parse cache
control extensions independently of X-Accel-Expires, can be
implemented by using just one flag.

Patch below, review and testing appreciated:

# HG changeset patch
# User Maxim Dounin <mdounin@mdounin.ru>
# Date 1650775261 -10800
# Sun Apr 24 07:41:01 2022 +0300
# Node ID f9c6d561b510e6008bfb4d7989f358dba33b38cd
# Parent a736a7a613ea6e182ff86fbadcb98bb0f8891c0b
Upstream: fixed X-Accel-Expires/Cache-Control/Expires handling.

Previously, if caching was disabled due to Expires in the past, nginx
failed to cache the response even if it was cacheable as per subsequently
parsed Cache-Control header (ticket #964).

Similarly, if caching was disabled due to Expires in the past,
"Cache-Control: no-cache" or "Cache-Control: max-age=0", caching was not
used if it was cacheable as per subsequently parsed X-Accel-Expires header.

Fix is to avoid disabling caching immediately after parsing Expires in
the past, but rather set a flag which is later checked by
ngx_http_upstream_process_headers() (and cleared by "Cache-Control: max-age"
and X-Accel-Expires).

Additionally, now X-Accel-Expires does not prevent parsing of cache control
extensions, notably stale-while-revalidate and stale-if-error. This
ensures that order of the X-Accel-Expires and Cache-Control headers is not
important.

Prodded by Vadim Fedorenko and Yugo Horie.

diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c
--- a/src/http/ngx_http_upstream.c
+++ b/src/http/ngx_http_upstream.c
@@ -2697,6 +2697,10 @@ ngx_http_upstream_intercept_errors(ngx_h

if (r->cache) {

+ if (u->headers_in.no_cache) {
+ u->cacheable = 0;
+ }
+
if (u->cacheable) {
time_t valid;

@@ -2791,6 +2795,10 @@ ngx_http_upstream_process_headers(ngx_ht

umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);

+ if (u->headers_in.no_cache) {
+ u->cacheable = 0;
+ }
+
if (u->headers_in.x_accel_redirect
&& !(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_XA_REDIRECT))
{
@@ -4735,18 +4743,18 @@ ngx_http_upstream_process_cache_control(
return NGX_OK;
}

- if (r->cache->valid_sec != 0 && u->headers_in.x_accel_expires != NULL) {
- return NGX_OK;
- }
-
start = h->value.data;
last = start + h->value.len;

+ if (r->cache->valid_sec != 0 && u->headers_in.x_accel_expires != NULL) {
+ goto extensions;
+ }
+
if (ngx_strlcasestrn(start, last, (u_char *) "no-cache", 8 - 1) != NULL
|| ngx_strlcasestrn(start, last, (u_char *) "no-store", 8 - 1) != NULL
|| ngx_strlcasestrn(start, last, (u_char *) "private", 7 - 1) != NULL)
{
- u->cacheable = 0;
+ u->headers_in.no_cache = 1;
return NGX_OK;
}

@@ -4776,12 +4784,15 @@ ngx_http_upstream_process_cache_control(
}

if (n == 0) {
- u->cacheable = 0;
+ u->headers_in.no_cache = 1;
return NGX_OK;
}

r->cache->valid_sec = ngx_time() + n;
- }
+ u->headers_in.no_cache = 0;
+ }
+
+extensions:

p = ngx_strlcasestrn(start, last, (u_char *) "stale-while-revalidate=",
23 - 1);
@@ -4863,7 +4874,7 @@ ngx_http_upstream_process_expires(ngx_ht
expires = ngx_parse_http_time(h->value.data, h->value.len);

if (expires == NGX_ERROR || expires < ngx_time()) {
- u->cacheable = 0;
+ u->headers_in.no_cache = 1;
return NGX_OK;
}

@@ -4914,6 +4925,7 @@ ngx_http_upstream_process_accel_expires(

default:
r->cache->valid_sec = ngx_time() + n;
+ u->headers_in.no_cache = 0;
return NGX_OK;
}
}
@@ -4925,6 +4937,7 @@ ngx_http_upstream_process_accel_expires(

if (n != NGX_ERROR) {
r->cache->valid_sec = n;
+ u->headers_in.no_cache = 0;
}
}
#endif
diff --git a/src/http/ngx_http_upstream.h b/src/http/ngx_http_upstream.h
--- a/src/http/ngx_http_upstream.h
+++ b/src/http/ngx_http_upstream.h
@@ -297,6 +297,7 @@ typedef struct {

unsigned connection_close:1;
unsigned chunked:1;
+ unsigned no_cache:1;
} ngx_http_upstream_headers_in_t;



--
Maxim Dounin
http://mdounin.ru/
_______________________________________________
nginx-devel mailing list -- nginx-devel@nginx.org
To unsubscribe send an email to nginx-devel-leave@nginx.org
Subject Author Views Posted

[PATCH] Upstream: prioritise Cache-Control over Expires

Vadim Fedorenko via nginx-devel 1065 April 14, 2022 07:04PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 139 April 16, 2022 09:56PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Vadim Fedorenko via nginx-devel 159 April 17, 2022 03:52PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

yugo-horie 204 April 17, 2022 09:16PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Vadim Fedorenko via nginx-devel 187 April 18, 2022 06:22AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

yugo-horie 175 April 18, 2022 07:04PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 252 April 19, 2022 11:02AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 116 April 22, 2022 02:24PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Vadim Fedorenko via nginx-devel 221 April 22, 2022 03:08PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 138 April 24, 2022 12:56AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 126 April 24, 2022 11:44AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

yugo-horie 168 April 25, 2022 08:28AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Vadim Fedorenko via nginx-devel 162 April 25, 2022 05:04PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Vadim Fedorenko via nginx-devel 142 April 25, 2022 05:08PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Sergey Kandaurov 120 June 06, 2022 09:44AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Sergey Kandaurov 99 June 06, 2022 10:42AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 116 June 06, 2022 05:10PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Sergey Kandaurov 141 June 07, 2022 12:02PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 156 June 07, 2022 09:00PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Sergey Kandaurov 133 June 10, 2022 07:40AM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Maxim Dounin 112 June 11, 2022 06:54PM

Re: [PATCH] Upstream: prioritise Cache-Control over Expires

Sergey Kandaurov 140 June 13, 2022 07:42AM



Sorry, you do not have permission to post/reply in this forum.

Online Users

Guests: 239
Record Number of Users: 8 on April 13, 2023
Record Number of Guests: 421 on December 02, 2018
Powered by nginx      Powered by FreeBSD      PHP Powered      Powered by MariaDB      ipv6 ready