[PATCH 1 of 3] Correctly calculate and set Age header

Hiroaki Nakamura hnakamur at gmail.com
Fri Jun 14 07:29:14 UTC 2024


# HG changeset patch
# User Hiroaki Nakamura <hnakamur at gmail.com>
# Date 1718345844 -32400
#      Fri Jun 14 15:17:24 2024 +0900
# Branch correct_age
# Node ID 493817cf38580a71430f9c1293e29bdfbf243075
# Parent  2f636ec9c71aaa7129f3f79fa1e584e02f8748f0
Correctly calculate and set Age header.
Implement the calculation of the Age header as specified in
"RFC 9111: HTTP Caching"
https://www.rfc-editor.org/rfc/rfc9111.html

diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_cache.h
--- a/src/http/ngx_http_cache.h Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_cache.h Fri Jun 14 15:17:24 2024 +0900
@@ -59,6 +59,8 @@
     size_t                           body_start;
     off_t                            fs_size;
     ngx_msec_t                       lock_time;
+    time_t                           response_time;
+    time_t                           corrected_initial_age;
 } ngx_http_file_cache_node_t;


@@ -75,6 +77,8 @@
     time_t                           error_sec;
     time_t                           last_modified;
     time_t                           date;
+    time_t                           response_time;
+    time_t                           corrected_initial_age;

     ngx_str_t                        etag;
     ngx_str_t                        vary;
diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_file_cache.c
--- a/src/http/ngx_http_file_cache.c    Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_file_cache.c    Fri Jun 14 15:17:24 2024 +0900
@@ -971,6 +971,8 @@
     fcn->uniq = 0;
     fcn->body_start = 0;
     fcn->fs_size = 0;
+    fcn->response_time = 0;
+    fcn->corrected_initial_age = 0;

 done:

@@ -980,6 +982,8 @@

     c->uniq = fcn->uniq;
     c->error = fcn->error;
+    c->response_time = fcn->response_time;
+    c->corrected_initial_age = fcn->corrected_initial_age;
     c->node = fcn;

 failed:
@@ -1624,6 +1628,7 @@
 ngx_int_t
 ngx_http_cache_send(ngx_http_request_t *r)
 {
+    time_t             resident_time, current_age;
     ngx_int_t          rc;
     ngx_buf_t         *b;
     ngx_chain_t        out;
@@ -1646,6 +1651,17 @@
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }

+    /*
+     * Update age response header.
+     * https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age
+     */
+    resident_time = ngx_time() - c->response_time;
+    current_age = c->corrected_initial_age + resident_time;
+    r->headers_out.age_n = current_age;
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http file cache send, resp:%O, resident:%d, age:%d",
+                   c->response_time, resident_time, current_age);
+
     rc = ngx_http_send_header(r);

     if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_header_filter_module.c
--- a/src/http/ngx_http_header_filter_module.c  Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_header_filter_module.c  Fri Jun 14 15:17:24 2024 +0900
@@ -322,6 +322,10 @@
         len += sizeof("Last-Modified: Mon, 28 Sep 1970 06:00:00 GMT" CRLF) - 1;
     }

+    if (r->headers_out.age_n != -1) {
+        len += sizeof("Age: ") - 1 + NGX_OFF_T_LEN + 2;
+    }
+
     c = r->connection;

     if (r->headers_out.location
@@ -518,6 +522,10 @@
         *b->last++ = CR; *b->last++ = LF;
     }

+    if (r->headers_out.age_n != -1) {
+        b->last = ngx_sprintf(b->last, "Age: %O" CRLF, r->headers_out.age_n);
+    }
+
     if (host.data) {

         p = b->last + sizeof("Location: ") - 1;
diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_request.c
--- a/src/http/ngx_http_request.c       Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_request.c       Fri Jun 14 15:17:24 2024 +0900
@@ -646,6 +646,7 @@
     r->headers_in.keep_alive_n = -1;
     r->headers_out.content_length_n = -1;
     r->headers_out.last_modified_time = -1;
+    r->headers_out.age_n = -1;

     r->uri_changes = NGX_HTTP_MAX_URI_CHANGES + 1;
     r->subrequests = NGX_HTTP_MAX_SUBREQUESTS + 1;
diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_request.h
--- a/src/http/ngx_http_request.h       Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_request.h       Fri Jun 14 15:17:24 2024 +0900
@@ -290,6 +290,7 @@
     off_t                             content_offset;
     time_t                            date_time;
     time_t                            last_modified_time;
+    off_t                             age_n;
 } ngx_http_headers_out_t;


diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_special_response.c
--- a/src/http/ngx_http_special_response.c      Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_special_response.c      Fri Jun 14 15:17:24 2024 +0900
@@ -581,6 +581,7 @@

     r->headers_out.content_length_n = -1;
     r->headers_out.last_modified_time = -1;
+    r->headers_out.age_n = -1;
 }


diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_upstream.c
--- a/src/http/ngx_http_upstream.c      Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_upstream.c      Fri Jun 14 15:17:24 2024 +0900
@@ -50,6 +50,8 @@
     ngx_http_upstream_t *u);
 static ngx_int_t ngx_http_upstream_test_next(ngx_http_request_t *r,
     ngx_http_upstream_t *u);
+static void ngx_http_upstream_update_age(ngx_http_request_t *r,
+    ngx_http_upstream_t *u, time_t now);
 static ngx_int_t ngx_http_upstream_intercept_errors(ngx_http_request_t *r,
     ngx_http_upstream_t *u);
 static ngx_int_t ngx_http_upstream_test_connect(ngx_connection_t *c);
@@ -132,6 +134,8 @@
     ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_upstream_process_vary(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
+static ngx_int_t ngx_http_upstream_process_age(ngx_http_request_t *r,
+    ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_upstream_copy_header_line(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t
@@ -319,6 +323,10 @@
                  ngx_http_upstream_copy_header_line,
                  offsetof(ngx_http_headers_out_t, content_encoding), 0 },

+    { ngx_string("Age"),
+                 ngx_http_upstream_process_age, 0,
+                 ngx_http_upstream_ignore_header_line, 0, 0 },
+
     { ngx_null_string, NULL, 0, NULL, 0, 0 }
 };

@@ -499,6 +507,7 @@

     u->headers_in.content_length_n = -1;
     u->headers_in.last_modified_time = -1;
+    u->headers_in.age_n = -1;

     return NGX_OK;
 }
@@ -1068,6 +1077,7 @@
     ngx_memzero(&u->headers_in, sizeof(ngx_http_upstream_headers_in_t));
     u->headers_in.content_length_n = -1;
     u->headers_in.last_modified_time = -1;
+    u->headers_in.age_n = -1;

     if (ngx_list_init(&u->headers_in.headers, r->pool, 8,
                       sizeof(ngx_table_elt_t))
@@ -1547,6 +1557,7 @@
     ngx_memzero(u->state, sizeof(ngx_http_upstream_state_t));

     u->start_time = ngx_current_msec;
+    u->request_time = ngx_time();

     u->state->response_time = (ngx_msec_t) -1;
     u->state->connect_time = (ngx_msec_t) -1;
@@ -2006,6 +2017,7 @@
     ngx_memzero(&u->headers_in, sizeof(ngx_http_upstream_headers_in_t));
     u->headers_in.content_length_n = -1;
     u->headers_in.last_modified_time = -1;
+    u->headers_in.age_n = -1;

     if (ngx_list_init(&u->headers_in.headers, r->pool, 8,
                       sizeof(ngx_table_elt_t))
@@ -2520,6 +2532,8 @@
         return;
     }

+    ngx_http_upstream_update_age(r, u, ngx_time());
+
     ngx_http_upstream_send_response(r, u);
 }

@@ -2606,6 +2620,7 @@
                        "http upstream not modified");

         now = ngx_time();
+        ngx_http_upstream_update_age(r, u, now);

         valid = r->cache->valid_sec;
         updating = r->cache->updating_sec;
@@ -2639,7 +2654,12 @@
             valid = ngx_http_file_cache_valid(u->conf->cache_valid,
                                               u->headers_in.status_n);
             if (valid) {
-                valid = now + valid;
+                ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                               "adjust cache valid_sec:%O, "
+                               "valid:%O, init_age:%d for 304",
+                               now + valid - r->cache->corrected_initial_age,
+                               valid, r->cache->corrected_initial_age);
+                valid = now + valid - r->cache->corrected_initial_age;
             }
         }

@@ -2663,6 +2683,59 @@
 }


+static void
+ngx_http_upstream_update_age(ngx_http_request_t *r, ngx_http_upstream_t *u,
+    time_t now)
+{
+    time_t  response_time, date, apparent_age, response_delay, age_value,
+            corrected_age_value, corrected_initial_age;
+
+    /*
+     * Update age response header.
+     * https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age
+     */
+    response_time = now;
+    if (u->headers_in.date != NULL) {
+        date = ngx_parse_http_time(u->headers_in.date->value.data,
+                                   u->headers_in.date->value.len);
+        if (date == NGX_ERROR) {
+            date = now;
+        }
+    } else {
+        date = now;
+    }
+    apparent_age = ngx_max(0, response_time - date);
+
+    response_delay = response_time - u->request_time;
+    age_value = u->headers_in.age_n != -1 ? u->headers_in.age_n : 0;
+    corrected_age_value = age_value + response_delay;
+
+    corrected_initial_age = ngx_max(apparent_age, corrected_age_value);
+    r->headers_out.age_n = corrected_initial_age;
+
+    ngx_log_debug8(NGX_LOG_DEBUG_HTTP, u->peer.connection->log, 0,
+                   "http upstream set age:%O, req:%O, resp:%O, date:%O, "
+                   "a_age:%O, resp_delay:%O, u_age:%O, c_age:%O",
+                   corrected_initial_age, u->request_time, response_time, date,
+                   apparent_age, response_delay, u->headers_in.age_n,
+                   corrected_age_value);
+
+#if (NGX_HTTP_CACHE)
+    if (r->cache) {
+        r->cache->response_time = response_time;
+        r->cache->corrected_initial_age = corrected_initial_age;
+        if (u->headers_in.adjusting_valid_sec) {
+            r->cache->valid_sec -= corrected_initial_age;
+            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, u->peer.connection->log, 0,
+                           "http upstream adjusted cache "
+                           "valid_sec:%O, init_age:%O",
+                           r->cache->valid_sec, corrected_initial_age);
+        }
+    }
+#endif
+}
+
+
 static ngx_int_t
 ngx_http_upstream_intercept_errors(ngx_http_request_t *r,
     ngx_http_upstream_t *u)
@@ -2738,6 +2811,7 @@
                                                           status);
                         if (valid) {
                             r->cache->valid_sec = ngx_time() + valid;
+                            u->headers_in.adjusting_valid_sec = 1;
                         }
                     }

@@ -2945,6 +3019,7 @@
     r->headers_out.status_line = u->headers_in.status_line;

     r->headers_out.content_length_n = u->headers_in.content_length_n;
+    r->headers_out.age_n = u->headers_in.age_n;

     r->disable_not_modified = !u->cacheable;

@@ -4609,6 +4684,7 @@

                 if (valid) {
                     r->cache->valid_sec = ngx_time() + valid;
+                    u->headers_in.adjusting_valid_sec = 1;
                     r->cache->error = rc;
                 }
             }
@@ -4884,6 +4960,7 @@
         }

         r->cache->valid_sec = ngx_time() + n;
+        u->headers_in.adjusting_valid_sec = 1;
         u->headers_in.expired = 0;
     }

@@ -5046,6 +5123,7 @@

         default:
             r->cache->valid_sec = ngx_time() + n;
+            u->headers_in.adjusting_valid_sec = 1;
             u->headers_in.no_cache = 0;
             u->headers_in.expired = 0;
             return NGX_OK;
@@ -5312,6 +5390,39 @@


 static ngx_int_t
+ngx_http_upstream_process_age(ngx_http_request_t *r,
+    ngx_table_elt_t *h, ngx_uint_t offset)
+{
+    ngx_http_upstream_t  *u;
+
+    u = r->upstream;
+
+    if (u->headers_in.age) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent duplicate header line: "%V: %V", "
+                      "previous value: "%V: %V"",
+                      &h->key, &h->value,
+                      &u->headers_in.age->key,
+                      &u->headers_in.age->value);
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
+    }
+
+    h->next = NULL;
+    u->headers_in.age = h;
+    u->headers_in.age_n = ngx_atoof(h->value.data, h->value.len);
+
+    if (u->headers_in.age_n == NGX_ERROR) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent invalid "Age" header: "
+                      ""%V: %V"", &h->key, &h->value);
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
 ngx_http_upstream_copy_header_line(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_uint_t offset)
 {
diff -r 2f636ec9c71a -r 493817cf3858 src/http/ngx_http_upstream.h
--- a/src/http/ngx_http_upstream.h      Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/ngx_http_upstream.h      Fri Jun 14 15:17:24 2024 +0900
@@ -287,14 +287,17 @@

     ngx_table_elt_t                 *cache_control;
     ngx_table_elt_t                 *set_cookie;
+    ngx_table_elt_t                 *age;

     off_t                            content_length_n;
     time_t                           last_modified_time;
+    off_t                            age_n;

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


@@ -369,6 +372,7 @@
                                          ngx_table_elt_t *h);

     ngx_msec_t                       start_time;
+    time_t                           request_time;

     ngx_http_upstream_state_t       *state;

diff -r 2f636ec9c71a -r 493817cf3858 src/http/v2/ngx_http_v2.h
--- a/src/http/v2/ngx_http_v2.h Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/v2/ngx_http_v2.h Fri Jun 14 15:17:24 2024 +0900
@@ -398,6 +398,7 @@
 #define NGX_HTTP_V2_STATUS_404_INDEX      13
 #define NGX_HTTP_V2_STATUS_500_INDEX      14

+#define NGX_HTTP_V2_AGE_INDEX             21
 #define NGX_HTTP_V2_CONTENT_LENGTH_INDEX  28
 #define NGX_HTTP_V2_CONTENT_TYPE_INDEX    31
 #define NGX_HTTP_V2_DATE_INDEX            33
diff -r 2f636ec9c71a -r 493817cf3858 src/http/v2/ngx_http_v2_filter_module.c
--- a/src/http/v2/ngx_http_v2_filter_module.c   Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/v2/ngx_http_v2_filter_module.c   Fri Jun 14 15:17:24 2024 +0900
@@ -256,6 +256,10 @@
         len += 1 + ngx_http_v2_literal_size("Wed, 31 Dec 1986 18:00:00 GMT");
     }

+    if (r->headers_out.age_n != -1) {
+        len += 1 + ngx_http_v2_integer_octets(NGX_OFF_T_LEN) + NGX_OFF_T_LEN;
+    }
+
     if (r->headers_out.location && r->headers_out.location->value.len) {

         if (r->headers_out.location->value.data[0] == '/'
@@ -543,6 +547,18 @@
         pos = ngx_http_v2_write_value(pos, pos, len, tmp);
     }

+    if (r->headers_out.age_n != -1) {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0,
+                       "http2 output header: "age: %O"",
+                       r->headers_out.age_n);
+
+        *pos++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_AGE_INDEX);
+
+        p = pos;
+        pos = ngx_sprintf(pos + 1, "%O", r->headers_out.age_n);
+        *p = NGX_HTTP_V2_ENCODE_RAW | (u_char) (pos - p - 1);
+    }
+
     if (r->headers_out.location && r->headers_out.location->value.len) {
         ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0,
                        "http2 output header: "location: %V"",
diff -r 2f636ec9c71a -r 493817cf3858 src/http/v3/ngx_http_v3_filter_module.c
--- a/src/http/v3/ngx_http_v3_filter_module.c   Fri Jun 14 15:17:14 2024 +0900
+++ b/src/http/v3/ngx_http_v3_filter_module.c   Fri Jun 14 15:17:24 2024 +0900
@@ -13,6 +13,7 @@
 /* static table indices */
 #define NGX_HTTP_V3_HEADER_AUTHORITY                 0
 #define NGX_HTTP_V3_HEADER_PATH_ROOT                 1
+#define NGX_HTTP_V3_HEADER_AGE_ZERO                  2
 #define NGX_HTTP_V3_HEADER_CONTENT_LENGTH_ZERO       4
 #define NGX_HTTP_V3_HEADER_DATE                      6
 #define NGX_HTTP_V3_HEADER_LAST_MODIFIED             10
@@ -213,6 +214,15 @@
                                   sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1);
     }

+    if (r->headers_out.age_n > 0) {
+        len += ngx_http_v3_encode_field_lri(NULL, 0,
+                                            NGX_HTTP_V3_HEADER_AGE_ZERO,
+                                            NULL, NGX_OFF_T_LEN);
+    } else if (r->headers_out.age_n == 0) {
+        len += ngx_http_v3_encode_field_ri(NULL, 0,
+                                           NGX_HTTP_V3_HEADER_AGE_ZERO);
+    }
+
     if (r->headers_out.location && r->headers_out.location->value.len) {

         if (r->headers_out.location->value.data[0] == '/'
@@ -452,6 +462,27 @@
                                               p, n);
     }

+    if (r->headers_out.age_n != -1) {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: "age: %O"",
+                       r->headers_out.age_n);
+
+        if (r->headers_out.age_n > 0) {
+            p = ngx_sprintf(b->last, "%O", r->headers_out.age_n);
+            n = p - b->last;
+
+            b->last = (u_char *) ngx_http_v3_encode_field_lri(b->last, 0,
+                                                   NGX_HTTP_V3_HEADER_AGE_ZERO,
+                                                   NULL, n);
+
+            b->last = ngx_sprintf(b->last, "%O", r->headers_out.age_n);
+
+        } else {
+            b->last = (u_char *) ngx_http_v3_encode_field_ri(b->last, 0,
+                                                  NGX_HTTP_V3_HEADER_AGE_ZERO);
+        }
+    }
+
     if (r->headers_out.location && r->headers_out.location->value.len) {
         ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                        "http3 output header: "location: %V"",


More information about the nginx-devel mailing list