# HG changeset patch # User Hiroaki Nakamura <hnaka...@gmail.com> # Date 1719322447 -32400 # Tue Jun 25 22:34:07 2024 +0900 # Node ID a1e4c426f1063c7a2ab30e60df7e1756ae7d8918 # Parent 6e76116ca337e24cf0d807e042e40c96344cd22e Cache: added calculation of the Age header. * Implement the modified calculation of the Age header as specified in RFC 9111 "HTTP Caching", https://www.rfc-editor.org/rfc/rfc9111.html
Calculation initial age as specified in RFC 9111: apparent_age = max(0, response_time - date_value); response_delay = response_time - request_time; corrected_age_value = age_value + response_delay; corrected_initial_age = max(apparent_age, corrected_age_value); However, if the upstream date is too old, then apparant_age becomes too big. Therefore, we use the following calculation: if upstream date is between request_time and response_time, we assume the upstream's clock is reasonably well synchronized and use corrected_initial_age = age_value + (response_time - date); if upstream date is not present or not between request_time and response_time, we do not rely on the upstream date and use corrected_initial_age = age_value + (response_time - u->request_time); When sending a cached response, we calculate the Age as specified in RFC 9111: resident_time = now - response_time; current_age = corrected_initial_age + resident_time; where response_time is cache_creation_time. * Also ignore Expires and X-Accel-Expires when Cache-Control max-age or s-maxage is set as specified in https://www.rfc-editor.org/rfc/rfc9111#name-expires: "If a response includes a Cache-Control header field with the max-age directive (Section 5.2.2.1), a recipient MUST ignore the Expires header field. Likewise, if a response includes the s-maxage directive (Section 5.2.2.10), a shared cache recipient MUST ignore the Expires header field." diff -r 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_cache.h --- a/src/http/ngx_http_cache.h Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_cache.h Tue Jun 25 22:34:07 2024 +0900 @@ -27,7 +27,7 @@ #define NGX_HTTP_CACHE_ETAG_LEN 128 #define NGX_HTTP_CACHE_VARY_LEN 128 -#define NGX_HTTP_CACHE_VERSION 5 +#define NGX_HTTP_CACHE_VERSION 6 typedef struct { @@ -59,6 +59,8 @@ typedef struct { size_t body_start; off_t fs_size; ngx_msec_t lock_time; + time_t creation_time; + time_t corrected_initial_age; } ngx_http_file_cache_node_t; @@ -75,6 +77,8 @@ struct ngx_http_cache_s { time_t error_sec; time_t last_modified; time_t date; + time_t creation_time; + time_t corrected_initial_age; ngx_str_t etag; ngx_str_t vary; @@ -141,6 +145,8 @@ typedef struct { u_char vary_len; u_char vary[NGX_HTTP_CACHE_VARY_LEN]; u_char variant[NGX_HTTP_CACHE_KEY_LEN]; + time_t creation_time; + time_t corrected_initial_age; } ngx_http_file_cache_header_t; diff -r 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_file_cache.c --- a/src/http/ngx_http_file_cache.c Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_file_cache.c Tue Jun 25 22:34:07 2024 +0900 @@ -627,6 +627,8 @@ ngx_http_file_cache_read(ngx_http_reques c->body_start = h->body_start; c->etag.len = h->etag_len; c->etag.data = h->etag; + c->creation_time = h->creation_time; + c->corrected_initial_age = h->corrected_initial_age; r->cached = 1; @@ -971,6 +973,8 @@ renew: fcn->uniq = 0; fcn->body_start = 0; fcn->fs_size = 0; + fcn->creation_time = 0; + fcn->corrected_initial_age = 0; done: @@ -980,6 +984,8 @@ done: c->uniq = fcn->uniq; c->error = fcn->error; + c->creation_time = fcn->creation_time; + c->corrected_initial_age = fcn->corrected_initial_age; c->node = fcn; failed: @@ -1326,6 +1332,8 @@ ngx_http_file_cache_set_header(ngx_http_ h->valid_msec = (u_short) c->valid_msec; h->header_start = (u_short) c->header_start; h->body_start = (u_short) c->body_start; + h->creation_time = c->creation_time; + h->corrected_initial_age = c->corrected_initial_age; if (c->etag.len <= NGX_HTTP_CACHE_ETAG_LEN) { h->etag_len = (u_char) c->etag.len; @@ -1590,6 +1598,8 @@ ngx_http_file_cache_update_header(ngx_ht h.valid_msec = (u_short) c->valid_msec; h.header_start = (u_short) c->header_start; h.body_start = (u_short) c->body_start; + h.creation_time = c->creation_time; + h.corrected_initial_age = c->corrected_initial_age; if (c->etag.len <= NGX_HTTP_CACHE_ETAG_LEN) { h.etag_len = (u_char) c->etag.len; diff -r 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_header_filter_module.c --- a/src/http/ngx_http_header_filter_module.c Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_header_filter_module.c Tue Jun 25 22:34:07 2024 +0900 @@ -322,6 +322,10 @@ ngx_http_header_filter(ngx_http_request_ 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_TIME_T_LEN + 2; + } + c = r->connection; if (r->headers_out.location @@ -518,6 +522,10 @@ ngx_http_header_filter(ngx_http_request_ *b->last++ = CR; *b->last++ = LF; } + if (r->headers_out.age_n != -1) { + b->last = ngx_sprintf(b->last, "Age: %T" CRLF, r->headers_out.age_n); + } + if (host.data) { p = b->last + sizeof("Location: ") - 1; diff -r 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_request.c --- a/src/http/ngx_http_request.c Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_request.c Tue Jun 25 22:34:07 2024 +0900 @@ -646,6 +646,7 @@ ngx_http_alloc_request(ngx_connection_t 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 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_request.h --- a/src/http/ngx_http_request.h Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_request.h Tue Jun 25 22:34:07 2024 +0900 @@ -291,6 +291,7 @@ typedef struct { off_t content_offset; time_t date_time; time_t last_modified_time; + time_t age_n; } ngx_http_headers_out_t; diff -r 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_special_response.c --- a/src/http/ngx_http_special_response.c Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_special_response.c Tue Jun 25 22:34:07 2024 +0900 @@ -581,6 +581,7 @@ ngx_http_clean_header(ngx_http_request_t r->headers_out.content_length_n = -1; r->headers_out.last_modified_time = -1; + r->headers_out.age_n = -1; } diff -r 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_upstream.c --- a/src/http/ngx_http_upstream.c Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_upstream.c Tue Jun 25 22:34:07 2024 +0900 @@ -52,6 +52,8 @@ static void ngx_http_upstream_process_he 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); 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); @@ -134,6 +136,8 @@ static ngx_int_t 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 @@ -321,6 +325,10 @@ static ngx_http_upstream_header_t ngx_h 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 } }; @@ -505,6 +513,7 @@ ngx_http_upstream_create(ngx_http_reques u->headers_in.content_length_n = -1; u->headers_in.last_modified_time = -1; + u->headers_in.age_n = -1; return NGX_OK; } @@ -959,6 +968,7 @@ ngx_http_upstream_cache(ngx_http_request c->updating_sec = 0; c->error_sec = 0; + u->headers_in.relative_freshness = 0; u->buffer.start = NULL; u->cache_status = NGX_HTTP_CACHE_EXPIRED; @@ -1074,6 +1084,7 @@ ngx_http_upstream_cache_send(ngx_http_re 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)) @@ -1555,6 +1566,7 @@ ngx_http_upstream_connect(ngx_http_reque 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; @@ -2014,6 +2026,7 @@ ngx_http_upstream_reinit(ngx_http_reques 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)) @@ -2541,6 +2554,8 @@ ngx_http_upstream_process_header(ngx_htt return; } + ngx_http_upstream_update_age(r, u); + ngx_http_upstream_send_response(r, u); } @@ -2627,6 +2642,7 @@ ngx_http_upstream_test_next(ngx_http_req "http upstream not modified"); now = ngx_time(); + ngx_http_upstream_update_age(r, u); valid = r->cache->valid_sec; updating = r->cache->updating_sec; @@ -2660,7 +2676,12 @@ ngx_http_upstream_test_next(ngx_http_req 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:%T, " + "valid:%T, 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; } } @@ -2684,6 +2705,77 @@ ngx_http_upstream_test_next(ngx_http_req } +static void +ngx_http_upstream_update_age(ngx_http_request_t *r, ngx_http_upstream_t *u) +{ + time_t response_time, age_value, date, corrected_initial_age; + + /* + * Update age response header. + * https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age + * + * apparent_age = max(0, response_time - date_value); + * + * response_delay = response_time - request_time; + * corrected_age_value = age_value + response_delay; + * + * corrected_initial_age = max(apparent_age, corrected_age_value); + * + * However, if the upstream date is too old, then apparant_age + * becomes too big. + * + * Therefore, we use the following calculation: + * + * if upstream date is between request_time and response_time, we assume + * the upstream's clock is reasonably well synchronized and use + * + * corrected_initial_age = age_value + (response_time - date); + * + * if upstream date is not present or not between request_time + * and response_time, we do not rely on the upstream date and use + * + * corrected_initial_age = age_value + (response_time - u->request_time); + */ + response_time = ngx_time(); + + age_value = u->headers_in.age_n != -1 ? u->headers_in.age_n : 0; + + if (u->headers_in.date != NULL) { + date = ngx_parse_http_time(u->headers_in.date->value.data, + u->headers_in.date->value.len); + } else { + date = NGX_ERROR; + } + + if (date >= u->request_time && date <= response_time) { + corrected_initial_age = age_value + (response_time - date); + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, u->peer.connection->log, 0, + "http upstream set age:%T, upstream date ok", + corrected_initial_age); + } else { + corrected_initial_age = age_value + (response_time - u->request_time); + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, u->peer.connection->log, 0, + "http upstream set age:%T, upstream date not ok", + corrected_initial_age); + } + + r->headers_out.age_n = corrected_initial_age ? corrected_initial_age : -1; + +#if (NGX_HTTP_CACHE) + if (r->cache) { + r->cache->creation_time = response_time; + r->cache->corrected_initial_age = corrected_initial_age; + if (u->headers_in.relative_freshness) { + r->cache->valid_sec -= corrected_initial_age; + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, u->peer.connection->log, 0, + "http upstream adjusted cache valid_sec:%T", + r->cache->valid_sec); + } + } +#endif +} + + static ngx_int_t ngx_http_upstream_intercept_errors(ngx_http_request_t *r, ngx_http_upstream_t *u) @@ -2759,6 +2851,7 @@ ngx_http_upstream_intercept_errors(ngx_h status); if (valid) { r->cache->valid_sec = ngx_time() + valid; + u->headers_in.relative_freshness = 1; } } @@ -2964,6 +3057,31 @@ ngx_http_upstream_process_headers(ngx_ht 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; + +#if (NGX_HTTP_CACHE) + if (r->cache) { + time_t resident_time, current_age; + ngx_http_cache_t *c; + + c = r->cache; + + /* + * Update age response header. + * https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age + * + * resident_time = now - response_time; + * current_age = corrected_initial_age + resident_time; + */ + resident_time = ngx_time() - c->creation_time; + current_age = c->corrected_initial_age + resident_time; + r->headers_out.age_n = current_age ? current_age : -1; + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http upstream process headers, created:%T, " + "resident:%T, age:%T", + c->creation_time, resident_time, current_age); + } +#endif r->disable_not_modified = !u->cacheable; @@ -3196,6 +3314,7 @@ ngx_http_upstream_send_response(ngx_http u->headers_in.status_n); if (valid) { r->cache->valid_sec = now + valid; + u->headers_in.relative_freshness = 1; } } @@ -4628,6 +4747,7 @@ ngx_http_upstream_finalize_request(ngx_h if (valid) { r->cache->valid_sec = ngx_time() + valid; + u->headers_in.relative_freshness = 1; r->cache->error = rc; } } @@ -4903,6 +5023,7 @@ ngx_http_upstream_process_cache_control( } r->cache->valid_sec = ngx_time() + n; + u->headers_in.relative_freshness = 1; u->headers_in.expired = 0; } @@ -5005,7 +5126,18 @@ ngx_http_upstream_process_expires(ngx_ht return NGX_OK; } - r->cache->valid_sec = expires; + /* + * https://www.rfc-editor.org/rfc/rfc9111#name-expires + * + * If a response includes a Cache-Control header field with the max-age + * directive (Section 5.2.2.1), a recipient MUST ignore the Expires + * header field. Likewise, if a response includes the s-maxage directive + * (Section 5.2.2.10), a shared cache recipient MUST ignore the Expires + * header field. + */ + if (!u->headers_in.relative_freshness) { + r->cache->valid_sec = expires; + } } #endif @@ -5065,6 +5197,7 @@ ngx_http_upstream_process_accel_expires( default: r->cache->valid_sec = ngx_time() + n; + u->headers_in.relative_freshness = 1; u->headers_in.no_cache = 0; u->headers_in.expired = 0; return NGX_OK; @@ -5077,7 +5210,18 @@ ngx_http_upstream_process_accel_expires( n = ngx_atoi(p, len); if (n != NGX_ERROR) { - r->cache->valid_sec = n; + /* + * https://www.rfc-editor.org/rfc/rfc9111#name-expires + * + * If a response includes a Cache-Control header field with the max-age + * directive (Section 5.2.2.1), a recipient MUST ignore the Expires + * header field. Likewise, if a response includes the s-maxage directive + * (Section 5.2.2.10), a shared cache recipient MUST ignore the Expires + * header field. + */ + if (!u->headers_in.relative_freshness) { + r->cache->valid_sec = n; + } u->headers_in.no_cache = 0; u->headers_in.expired = 0; } @@ -5331,6 +5475,38 @@ ngx_http_upstream_process_vary(ngx_http_ 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_WARN, r->connection->log, 0, + "ignore duplicate age header from upstream: \"%V: %V\", " + "previous value: \"%V: %V\"", + &h->key, &h->value, + &u->headers_in.age->key, + &u->headers_in.age->value); + return NGX_OK; + } + + 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_WARN, r->connection->log, 0, + "ignore invalid \"Age\" header from upstream: " + "\"%V: %V\"", &h->key, &h->value); + } + + 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 6e76116ca337 -r a1e4c426f106 src/http/ngx_http_upstream.h --- a/src/http/ngx_http_upstream.h Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/ngx_http_upstream.h Tue Jun 25 22:34:07 2024 +0900 @@ -287,14 +287,17 @@ typedef struct { 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; + time_t age_n; unsigned connection_close:1; unsigned chunked:1; unsigned no_cache:1; unsigned expired:1; + unsigned relative_freshness:1; } ngx_http_upstream_headers_in_t; @@ -369,6 +372,7 @@ struct ngx_http_upstream_s { ngx_table_elt_t *h); ngx_msec_t start_time; + time_t request_time; ngx_http_upstream_state_t *state; diff -r 6e76116ca337 -r a1e4c426f106 src/http/v2/ngx_http_v2.h --- a/src/http/v2/ngx_http_v2.h Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/v2/ngx_http_v2.h Tue Jun 25 22:34:07 2024 +0900 @@ -398,6 +398,7 @@ ngx_int_t ngx_http_v2_table_size(ngx_htt #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 6e76116ca337 -r a1e4c426f106 src/http/v2/ngx_http_v2_filter_module.c --- a/src/http/v2/ngx_http_v2_filter_module.c Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/v2/ngx_http_v2_filter_module.c Tue Jun 25 22:34:07 2024 +0900 @@ -258,6 +258,10 @@ ngx_http_v2_header_filter(ngx_http_reque 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_TIME_T_LEN) + NGX_TIME_T_LEN; + } + if (r->headers_out.location && r->headers_out.location->value.len) { if (r->headers_out.location->value.data[0] == '/' @@ -552,6 +556,18 @@ ngx_http_v2_header_filter(ngx_http_reque 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: %T\"", + r->headers_out.age_n); + + *pos++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_AGE_INDEX); + + p = pos; + pos = ngx_sprintf(pos + 1, "%T", 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 6e76116ca337 -r a1e4c426f106 src/http/v3/ngx_http_v3_filter_module.c --- a/src/http/v3/ngx_http_v3_filter_module.c Tue Jun 25 22:33:57 2024 +0900 +++ b/src/http/v3/ngx_http_v3_filter_module.c Tue Jun 25 22:34:07 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 @@ ngx_http_v3_header_filter(ngx_http_reque 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_TIME_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 @@ ngx_http_v3_header_filter(ngx_http_reque p, n); } + if (r->headers_out.age_n != -1) { + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, + "http3 output header: \"age: %T\"", + r->headers_out.age_n); + + if (r->headers_out.age_n > 0) { + p = ngx_sprintf(b->last, "%T", 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, "%T", 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\"",