From bff1fce3cf65fda0f218a6719fca768229f0785f Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Wed, 19 Nov 2025 23:24:42 +0000 Subject: [PATCH 1/3] Implement RFC 9213 Targeted HTTP Cache Control This adds support for targeted Cache-Control headers (like CDN-Cache-Control) that allow cache directives to be targeted at specific caches. The implementation includes a configurable, priority-ordered list of targeted headers via proxy.config.http.cache.targeted_cache_control_headers, which is overridable per-remap rule. When a targeted header is present, it takes precedence over the standard Cache-Control header for caching decisions. Targeted headers are passed through downstream to allow proper cache hierarchy behavior. Fixes: #9113 --- .../configuration/cache-basics.en.rst | 78 ++++ doc/admin-guide/files/records.yaml.en.rst | 25 ++ doc/admin-guide/plugins/lua.en.rst | 1 + .../functions/TSHttpOverridableConfig.en.rst | 1 + .../api/types/TSOverridableConfigKey.en.rst | 1 + include/proxy/hdrs/MIME.h | 2 +- include/proxy/http/HttpConfig.h | 3 + include/proxy/http/OverridableConfigDefs.h | 3 +- include/ts/apidefs.h.in | 1 + src/api/InkAPI.cc | 9 + src/proxy/hdrs/MIME.cc | 30 +- src/proxy/http/HttpConfig.cc | 15 +- src/proxy/http/HttpSM.cc | 11 +- src/records/RecordsConfig.cc | 2 + .../replay/targeted-cache-control.replay.yaml | 389 ++++++++++++++++++ .../cache/targeted-cache-control.test.py | 25 ++ 16 files changed, 583 insertions(+), 13 deletions(-) create mode 100644 tests/gold_tests/cache/replay/targeted-cache-control.replay.yaml create mode 100644 tests/gold_tests/cache/targeted-cache-control.test.py diff --git a/doc/admin-guide/configuration/cache-basics.en.rst b/doc/admin-guide/configuration/cache-basics.en.rst index b3b71898179..b03d2bdae00 100644 --- a/doc/admin-guide/configuration/cache-basics.en.rst +++ b/doc/admin-guide/configuration/cache-basics.en.rst @@ -234,6 +234,84 @@ Traffic Server applies ``Cache-Control`` servability criteria after HTTP freshness criteria. For example, an object might be considered fresh but will not be served if its age is greater than its ``max-age``. +Targeted Cache Control (RFC 9213) +---------------------------------- + +Traffic Server supports `RFC 9213 `_ +Targeted HTTP Cache Control, which allows origin servers to provide different +cache directives for different classes of caches. This is particularly useful in CDN deployments where you want to +give different caching instructions to CDN caches versus browser caches. + +For example, an origin server might send:: + + Cache-Control: max-age=60 + CDN-Cache-Control: max-age=3600 + +When targeted cache control is enabled (via +:ts:cv:`proxy.config.http.cache.targeted_cache_control_headers`), Traffic +Server will use the ``CDN-Cache-Control`` directives instead of the standard +``Cache-Control`` directives for caching decisions. The browser receiving the +response will see both headers and use the standard ``Cache-Control``, allowing +the object to be cached for 60 seconds in the browser but 3600 seconds in the CDN. + +Configuration +~~~~~~~~~~~~~ + +To enable targeted cache control, set +:ts:cv:`proxy.config.http.cache.targeted_cache_control_headers` to a +comma-separated list of header names to check in priority order:: + + # In records.yaml: + proxy.config.http.cache.targeted_cache_control_headers: CDN-Cache-Control + +Or with multiple targeted headers in priority order:: + + proxy.config.http.cache.targeted_cache_control_headers: ATS-Cache-Control,CDN-Cache-Control + +This configuration is overridable per-remap, allowing different rules for +different origins:: + + # In remap.config: + map / https://origin.example.com/ @plugin=conf_remap.so \ + @pparam=proxy.config.http.cache.targeted_cache_control_headers=CDN-Cache-Control + +Behavior +~~~~~~~~ + +- When a targeted header is found (first match in the priority list), its + directives replace the standard ``Cache-Control`` directives for all caching + decisions. + +- If no targeted headers are present or they are all empty, Traffic Server falls + back to the standard ``Cache-Control`` header. + +- Targeted headers are passed through to downstream caches, allowing CDN chains + to use the same directives. + +- All standard cache control directives are supported in targeted headers: + ``max-age``, ``s-maxage``, ``no-cache``, ``no-store``, ``private``, + ``must-revalidate``, etc. + +Use Cases +~~~~~~~~~ + +**CDN with origin cache**: An origin might have its own caching layer but want +CDNs to cache more aggressively:: + + Cache-Control: max-age=60 + CDN-Cache-Control: max-age=86400 + +**Different CDN policies**: Using multiple CDN providers with different needs:: + + Cache-Control: max-age=300 + CDN1-Cache-Control: max-age=3600 + CDN2-Cache-Control: max-age=1800 + +**Prevent CDN caching while allowing browser caching**:: + + Cache-Control: max-age=300 + CDN-Cache-Control: no-store + Revalidating HTTP Objects ------------------------- diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index e7057658d30..d30a54a3172 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -2518,6 +2518,31 @@ Cache Control ``Cache-Control: max-age``. ===== ====================================================================== +.. ts:cv:: CONFIG proxy.config.http.cache.targeted_cache_control_headers STRING "" + :reloadable: + :overridable: + + Comma-separated list of targeted cache control header names to check in priority + order before falling back to the standard ``Cache-Control`` header. This implements + `RFC 9213 `_ Targeted HTTP Cache Control. + When empty (the default), targeted cache control is disabled and only the standard + ``Cache-Control`` header is used. + + Example values: + + - ``CDN-Cache-Control`` - Use only CDN-Cache-Control if present + - ``ATS-Cache-Control,CDN-Cache-Control`` - Check ATS-Cache-Control first, then + CDN-Cache-Control, then fall back to Cache-Control + + When a targeted header is found, its directives are used rather than those in the + standard ``Cache-Control`` header for caching decisions. The targeted headers are + passed through to downstream caches. + + .. note:: + + This implementation uses the existing Cache-Control parser rather than the + strict RFC 8941 Structured Fields parser specified in RFC 9213. + .. ts:cv:: CONFIG proxy.config.http.cache.max_stale_age INT 604800 :reloadable: :overridable: diff --git a/doc/admin-guide/plugins/lua.en.rst b/doc/admin-guide/plugins/lua.en.rst index 08970e340ec..fd0552230f6 100644 --- a/doc/admin-guide/plugins/lua.en.rst +++ b/doc/admin-guide/plugins/lua.en.rst @@ -4743,6 +4743,7 @@ Http config constants TS_LUA_CONFIG_NET_SOCK_NOTSENT_LOWAT TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE TS_LUA_CONFIG_HTTP_CACHE_POST_METHOD + TS_LUA_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS TS_LUA_CONFIG_LAST_ENTRY :ref:`TOP ` diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst index 73865be169a..9a175f4e15f 100644 --- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst +++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst @@ -196,6 +196,7 @@ TSOverridableConfigKey Value Config :enumerator:`TS_CONFIG_NET_SOCK_NOTSENT_LOWAT` :ts:cv:`proxy.config.net.sock_notsent_lowat` :enumerator:`TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE` :ts:cv:`proxy.config.body_factory.response_suppression_mode` :enumerator:`TS_CONFIG_HTTP_CACHE_POST_METHOD` :ts:cv:`proxy.config.http.cache.post_method` +:enumerator:`TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS` :ts:cv:`proxy.config.http.cache.targeted_cache_control_headers` ====================================================================== ==================================================================== Examples diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst index 64c4b19571e..56d325e6192 100644 --- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst +++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst @@ -163,6 +163,7 @@ Enumeration Members .. enumerator:: TS_CONFIG_HTTP_NO_DNS_JUST_FORWARD_TO_PARENT .. enumerator:: TS_CONFIG_HTTP_CACHE_IGNORE_QUERY .. enumerator:: TS_CONFIG_HTTP_CACHE_POST_METHOD +.. enumerator:: TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS Description diff --git a/include/proxy/hdrs/MIME.h b/include/proxy/hdrs/MIME.h index 4bed80dc13c..6461d799803 100644 --- a/include/proxy/hdrs/MIME.h +++ b/include/proxy/hdrs/MIME.h @@ -325,7 +325,7 @@ struct MIMEHdrImpl : public HdrHeapObjImpl { void check_strings(HeapCheck *heaps, int num_heaps); // Cooked values - void recompute_cooked_stuff(MIMEField *changing_field_or_null = nullptr); + void recompute_cooked_stuff(MIMEField *changing_field_or_null = nullptr, const char *targeted_headers_str = nullptr); void recompute_accelerators_and_presence_bits(); // Utility diff --git a/include/proxy/http/HttpConfig.h b/include/proxy/http/HttpConfig.h index 85cb7ea433c..747ca31156a 100644 --- a/include/proxy/http/HttpConfig.h +++ b/include/proxy/http/HttpConfig.h @@ -550,6 +550,9 @@ struct OverridableHttpConfigParams { MgmtByte cache_range_write = 0; MgmtByte allow_multi_range = 0; + char *targeted_cache_control_headers = nullptr; // This does not get free'd by us! + size_t targeted_cache_control_headers_len = 0; // Updated when targeted headers are set. + MgmtByte ignore_accept_mismatch = 0; MgmtByte ignore_accept_language_mismatch = 0; MgmtByte ignore_accept_encoding_mismatch = 0; diff --git a/include/proxy/http/OverridableConfigDefs.h b/include/proxy/http/OverridableConfigDefs.h index 52440b40b19..9e29bcffb50 100644 --- a/include/proxy/http/OverridableConfigDefs.h +++ b/include/proxy/http/OverridableConfigDefs.h @@ -249,6 +249,7 @@ X(HTTP_NEGATIVE_CACHING_LIST, negative_caching_list, "proxy.config.http.negative_caching_list", STRING, HttpStatusCodeList_Conv) \ X(HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE, connect_attempts_retry_backoff_base, "proxy.config.http.connect_attempts_retry_backoff_base", INT, GENERIC) \ X(HTTP_NEGATIVE_REVALIDATING_LIST, negative_revalidating_list, "proxy.config.http.negative_revalidating_list", STRING, HttpStatusCodeList_Conv) \ - X(HTTP_CACHE_POST_METHOD, cache_post_method, "proxy.config.http.cache.post_method", INT, GENERIC) + X(HTTP_CACHE_POST_METHOD, cache_post_method, "proxy.config.http.cache.post_method", INT, GENERIC) \ + X(HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, targeted_cache_control_headers, "proxy.config.http.cache.targeted_cache_control_headers", STRING, GENERIC) // clang-format on diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 078ce0eb69c..5515aa7665e 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -908,6 +908,7 @@ enum TSOverridableConfigKey { TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE, TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIST, TS_CONFIG_HTTP_CACHE_POST_METHOD, + TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, TS_CONFIG_LAST_ENTRY, }; diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 1c832eb9dd6..d9628ed1da9 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7561,6 +7561,15 @@ TSHttpTxnConfigStringSet(TSHttpTxn txnp, TSOverridableConfigKey conf, const char s->t_state.my_txn_conf().host_res_data = std::get(parsed.parsed); } break; + case TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS: + if (value && length > 0) { + s->t_state.my_txn_conf().targeted_cache_control_headers = const_cast(value); + s->t_state.my_txn_conf().targeted_cache_control_headers_len = length; + } else { + s->t_state.my_txn_conf().targeted_cache_control_headers = nullptr; + s->t_state.my_txn_conf().targeted_cache_control_headers_len = 0; + } + break; default: { if (value && length > 0) { return _eval_conv(&(s->t_state.my_txn_conf()), conf, value, length); diff --git a/src/proxy/hdrs/MIME.cc b/src/proxy/hdrs/MIME.cc index 8eae5d78529..f2fa4fbb52b 100644 --- a/src/proxy/hdrs/MIME.cc +++ b/src/proxy/hdrs/MIME.cc @@ -3713,7 +3713,7 @@ MIMEHdrImpl::recompute_accelerators_and_presence_bits() //////////////////////////////////////////////////////// void -MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null) +MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null, const char *targeted_headers_str) { int len, tlen; const char *s; @@ -3725,13 +3725,33 @@ MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null) mime_hdr_cooked_stuff_init(this, changing_field_or_null); - ////////////////////////////////////////////////// - // (1) cook the Cache-Control header if present // - ////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + // (1) cook the Cache-Control header (or targeted variant) if present // + ///////////////////////////////////////////////////////////////////////////// // to be safe, recompute unless you know this call is for other cooked field if ((changing_field_or_null == nullptr) || (changing_field_or_null->m_wks_idx != MIME_WKSIDX_PRAGMA)) { - field = mime_hdr_field_find(this, static_cast(MIME_FIELD_CACHE_CONTROL)); + field = nullptr; + + // Check for targeted cache control headers first (in priority order). + if (targeted_headers_str && *targeted_headers_str) { + swoc::TextView config_view{targeted_headers_str}; + while (config_view) { + swoc::TextView header_name = config_view.take_prefix_at(',').trim_if(&isspace); + if (!header_name.empty()) { + field = mime_hdr_field_find(this, std::string_view{header_name.data(), header_name.size()}); + if (field) { + // Found a targeted header, use it and stop searching. + break; + } + } + } + } + + // If no targeted header was found, fall back to standard Cache-Control. + if (!field) { + field = mime_hdr_field_find(this, static_cast(MIME_FIELD_CACHE_CONTROL)); + } if (field) { // try pathpaths first -- unlike most other fastpaths, this one diff --git a/src/proxy/http/HttpConfig.cc b/src/proxy/http/HttpConfig.cc index d58f7d7c016..63ed5e45668 100644 --- a/src/proxy/http/HttpConfig.cc +++ b/src/proxy/http/HttpConfig.cc @@ -1093,6 +1093,10 @@ HttpConfig::startup() HttpEstablishStaticConfigByte(c.oride.cache_required_headers, "proxy.config.http.cache.required_headers"); HttpEstablishStaticConfigByte(c.oride.cache_range_lookup, "proxy.config.http.cache.range.lookup"); HttpEstablishStaticConfigByte(c.oride.cache_range_write, "proxy.config.http.cache.range.write"); + HttpEstablishStaticConfigStringAlloc(c.oride.targeted_cache_control_headers, + "proxy.config.http.cache.targeted_cache_control_headers"); + c.oride.targeted_cache_control_headers_len = + c.oride.targeted_cache_control_headers ? strlen(c.oride.targeted_cache_control_headers) : 0; HttpEstablishStaticConfigStringAlloc(c.connect_ports_string, "proxy.config.http.connect_ports"); @@ -1398,10 +1402,13 @@ HttpConfig::reconfigure() params->max_payload_iobuf_index = m_master.max_payload_iobuf_index; params->max_msg_iobuf_index = m_master.max_msg_iobuf_index; - params->oride.cache_required_headers = m_master.oride.cache_required_headers; - params->oride.cache_range_lookup = INT_TO_BOOL(m_master.oride.cache_range_lookup); - params->oride.cache_range_write = INT_TO_BOOL(m_master.oride.cache_range_write); - params->oride.allow_multi_range = m_master.oride.allow_multi_range; + params->oride.cache_required_headers = m_master.oride.cache_required_headers; + params->oride.cache_range_lookup = INT_TO_BOOL(m_master.oride.cache_range_lookup); + params->oride.cache_range_write = INT_TO_BOOL(m_master.oride.cache_range_write); + params->oride.targeted_cache_control_headers = ats_strdup(m_master.oride.targeted_cache_control_headers); + params->oride.targeted_cache_control_headers_len = + params->oride.targeted_cache_control_headers ? strlen(params->oride.targeted_cache_control_headers) : 0; + params->oride.allow_multi_range = m_master.oride.allow_multi_range; params->connect_ports_string = ats_strdup(m_master.connect_ports_string); params->connect_ports = parse_ports_list(params->connect_ports_string); diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index d9cfb662923..135601bf3e2 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -2100,14 +2100,20 @@ HttpSM::state_read_server_response_header(int event, void *data) } // fallthrough - case ParseResult::DONE: - + case ParseResult::DONE: { if (!t_state.hdr_info.server_response.check_hdr_implements()) { t_state.http_return_code = HTTPStatus::BAD_GATEWAY; call_transact_and_set_next_state(HttpTransact::BadRequest); break; } + // Recompute cooked cache control with targeted headers (pass nullptr if not configured). + const char *targeted_headers = + (t_state.txn_conf->targeted_cache_control_headers && t_state.txn_conf->targeted_cache_control_headers[0] != '\0') ? + t_state.txn_conf->targeted_cache_control_headers : + nullptr; + t_state.hdr_info.server_response.m_mime->recompute_cooked_stuff(nullptr, targeted_headers); + SMDbg(dbg_ctl_http_seq, "Done parsing server response header"); // Now that we know that we have all of the origin server @@ -2138,6 +2144,7 @@ HttpSM::state_read_server_response_header(int event, void *data) server_entry->read_vio->disable(); // Disable the read until we finish the tunnel } break; + } case ParseResult::CONT: ink_assert(server_entry->eos == false); server_entry->read_vio->reenable(); diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index 88526c08ad3..f0cefd209e3 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -643,6 +643,8 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.http.cache.range.write", RECD_INT, "0", RECU_NULL, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , + {RECT_CONFIG, "proxy.config.http.cache.targeted_cache_control_headers", RECD_STRING, "", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + , // ######################## // # heuristic expiration # diff --git a/tests/gold_tests/cache/replay/targeted-cache-control.replay.yaml b/tests/gold_tests/cache/replay/targeted-cache-control.replay.yaml new file mode 100644 index 00000000000..90300f79e30 --- /dev/null +++ b/tests/gold_tests/cache/replay/targeted-cache-control.replay.yaml @@ -0,0 +1,389 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test targeted cache control headers per RFC 9213 +# Test that CDN-Cache-Control overrides Cache-Control when configured + +meta: + version: "1.0" + +# Configuration section for autest integration +autest: + description: 'Test targeted cache control headers per RFC 9213' + + dns: + name: 'dns' + + server: + name: 'server' + + client: + name: 'client' + + ats: + name: 'ts' + enable_cache: true + + records_config: + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: 'http|cache' + proxy.config.http.cache.http: 1 + proxy.config.http.cache.required_headers: 0 + proxy.config.http.cache.targeted_cache_control_headers: 'ATS-Cache-Control,CDN-Cache-Control' + + remap_config: + - from: "http://example.com/" + to: "http://backend.example.com:{SERVER_HTTP_PORT}/" + + - from: "http://acme.com/" + to: "http://backend.acme.com:{SERVER_HTTP_PORT}/" + plugins: + - name: "conf_remap.so" + args: + - "proxy.config.http.cache.targeted_cache_control_headers=ACME-Cache-Control" + + +sessions: +- transactions: + + ############################################################################# + # Test 1: CDN-Cache-Control with higher max-age overrides Cache-Control + ############################################################################# + - client-request: + method: GET + url: /targeted/test1 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test1-request1] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, text/plain] + - [Content-Length, "14"] + - [Cache-Control, "max-age=1"] + - [CDN-Cache-Control, "max-age=30"] # Should be used. + - [Connection, close] + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "max-age=1", as: equal } ] + - [ CDN-Cache-Control, { value: "max-age=30", as: equal } ] + + ############################################################################# + # Test 2: Priority order - first targeted header wins (verify lowercase). + ############################################################################# + - client-request: + method: GET + url: /targeted/test2 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test2-request1] + + # Test lowercase *cache-control. + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, text/plain] + - [Content-Length, "14"] + - [cache-control, "no-store"] + - [cdn-cache-control, "no-store"] + - [ats-cache-control, "max-age=30"] # Should take precedence. Test lowercase. + - [Connection, close] + + proxy-response: + status: 200 + headers: + fields: + - [ cache-control, { value: "no-store", as: equal } ] + - [ cdn-cache-control, { value: "no-store", as: equal } ] + - [ ats-cache-control, { value: "max-age=30", as: equal } ] + + + ############################################################################# + # Test 3: no-store in targeted header + ############################################################################# + - client-request: + method: GET + url: /targeted/test3 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test3-request1] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, text/plain] + - [Content-Length, "14"] + - [Cache-Control, "max-age=3600"] + - [CDN-Cache-Control, "no-store"] # Should be used. + - [Connection, close] + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "max-age=3600", as: equal } ] + - [ CDN-Cache-Control, { value: "no-store", as: equal } ] + + ############################################################################# + # Test 4: Vanilla Cache-Control should still function. + ############################################################################# + - client-request: + method: GET + url: /targeted/test4 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test4-request1] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, text/plain] + - [Content-Length, "14"] + - [Cache-Control, "max-age=30"] + - [Connection, close] + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "max-age=30", as: equal } ] + + + ############################################################################# + # Test 5: conf_remap.so override with ACME-Cache-Control + ############################################################################# + - client-request: + method: GET + url: /acme/test1 + version: '1.1' + headers: + fields: + - [Host, acme.com] + - [uuid, acme-test1-request1] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, text/plain] + - [Content-Length, "14"] + - [Cache-Control, "no-store"] + - [CDN-Cache-Control, "no-store"] + - [ATS-Cache-Control, "no-store"] + - [ACME-Cache-Control, "max-age=30"] # Should be used due to override + - [Connection, close] + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "no-store", as: equal } ] + - [ CDN-Cache-Control, { value: "no-store", as: equal } ] + - [ ATS-Cache-Control, { value: "no-store", as: equal } ] + - [ ACME-Cache-Control, { value: "max-age=30", as: equal } ] + + ############################################################################# + # Test 6: Verify ACME-Cache-Control override works (no-cache) + ############################################################################# + - client-request: + method: GET + url: /acme/test2 + version: '1.1' + headers: + fields: + - [Host, acme.com] + - [uuid, acme-test2-request1] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, text/plain] + - [Content-Length, "14"] + - [Cache-Control, "max-age=3600"] + - [ACME-Cache-Control, "no-store"] # Should prevent caching + - [Connection, close] + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "max-age=3600", as: equal } ] + - [ ACME-Cache-Control, { value: "no-store", as: equal } ] + + ############################################################################# + # Now verify the correct cache behavior from above. + ############################################################################# + - client-request: + # Delay to exceed Cache-Control but not CDN-Cache-Control. + delay: 2s + + method: GET + url: /targeted/test1 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test1-request2] + + # Should not reach the origin. + server-response: + status: 404 + reason: Not Found + + # Expect the cached 200 response. + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "max-age=1", as: equal } ] + - [ CDN-Cache-Control, { value: "max-age=30", as: equal } ] + + - client-request: + method: GET + url: /targeted/test2 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test2-request2] + + # Should not reach the origin. + server-response: + status: 404 + reason: Not Found + + # Expect the cached 200 response since ATS-Cache-Control takes precedence + # over CDN-Cache-Control and Cache-Control. + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "no-store", as: equal } ] + - [ CDN-Cache-Control, { value: "no-store", as: equal } ] + - [ ATS-Cache-Control, { value: "max-age=30", as: equal } ] + + - client-request: + method: GET + url: /targeted/test3 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test3-request2] + + # Since CDN-Cache-Control is no-store, the request should reach the origin. + server-response: + status: 404 + reason: Not Found + + proxy-response: + status: 404 + headers: + fields: + - [ Cache-Control, { as: absent } ] + - [ CDN-Cache-Control, { as: absent } ] + + - client-request: + method: GET + url: /targeted/test4 + version: '1.1' + headers: + fields: + - [Host, example.com] + - [uuid, targeted-test4-request2] + + # Should not reach the origin. + server-response: + status: 404 + reason: Not Found + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "max-age=30", as: equal } ] + + # Verify that ACME-Cache-Control: max-age=30 allowed caching. + - client-request: + method: GET + url: /acme/test1 + version: '1.1' + headers: + fields: + - [Host, acme.com] + - [uuid, acme-test1-request2] + + # The origin should not receive the request since ATS will reply out of cache. + server-response: + status: 404 + reason: Not Found + + # Expect the cached 200 response. + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "no-store", as: equal } ] + - [ CDN-Cache-Control, { value: "no-store", as: equal } ] + - [ ATS-Cache-Control, { value: "no-store", as: equal } ] + - [ ACME-Cache-Control, { value: "max-age=30", as: equal } ] + + # On the other hand, verify that ACME-Cache-Control: no-store prevents caching. + - client-request: + method: GET + url: /acme/test2 + version: '1.1' + headers: + fields: + - [Host, acme.com] + - [uuid, acme-test2-request2] + + server-response: + status: 404 + reason: Not Found + + # Expect the 404 from the origin server, not the cached 200 response. + proxy-response: + status: 404 + headers: + fields: + - [ Cache-Control, { as: absent } ] + - [ CDN-Cache-Control, { as: absent } ] + - [ ATS-Cache-Control, { as: absent } ] + - [ ACME-Cache-Control, { as: absent } ] diff --git a/tests/gold_tests/cache/targeted-cache-control.test.py b/tests/gold_tests/cache/targeted-cache-control.test.py new file mode 100644 index 00000000000..3bcd84047b8 --- /dev/null +++ b/tests/gold_tests/cache/targeted-cache-control.test.py @@ -0,0 +1,25 @@ +'''Test targeted cache control headers per RFC 9213.''' + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Test.Summary = ''' +Test targeted cache control headers per RFC 9213. +Verifies that CDN-Cache-Control and other targeted headers can override +standard Cache-Control when properly configured. +''' + +Test.ATSReplayTest(replay_file="replay/targeted-cache-control.replay.yaml") From b2ee1ed06645742f805708fbb72840ba349f74cf Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Thu, 11 Dec 2025 04:25:55 +0000 Subject: [PATCH 2/3] WKS and custom config parsing updates --- include/proxy/hdrs/MIME.h | 3 +- include/proxy/http/HttpConfig.h | 40 ++++++++++++-- include/proxy/http/OverridableConfigDefs.h | 2 +- src/api/InkAPI.cc | 12 +++-- src/proxy/hdrs/HdrToken.cc | 6 ++- src/proxy/hdrs/MIME.cc | 17 ++---- src/proxy/http/HttpConfig.cc | 62 ++++++++++++++++++---- src/proxy/http/HttpSM.cc | 9 ++-- 8 files changed, 114 insertions(+), 37 deletions(-) diff --git a/include/proxy/hdrs/MIME.h b/include/proxy/hdrs/MIME.h index 6461d799803..555f540872c 100644 --- a/include/proxy/hdrs/MIME.h +++ b/include/proxy/hdrs/MIME.h @@ -24,6 +24,7 @@ #pragma once #include +#include #include #include @@ -325,7 +326,7 @@ struct MIMEHdrImpl : public HdrHeapObjImpl { void check_strings(HeapCheck *heaps, int num_heaps); // Cooked values - void recompute_cooked_stuff(MIMEField *changing_field_or_null = nullptr, const char *targeted_headers_str = nullptr); + void recompute_cooked_stuff(MIMEField *changing_field_or_null = nullptr, std::span targeted_headers = {}); void recompute_accelerators_and_presence_bits(); // Utility diff --git a/include/proxy/http/HttpConfig.h b/include/proxy/http/HttpConfig.h index 747ca31156a..8580fa3563c 100644 --- a/include/proxy/http/HttpConfig.h +++ b/include/proxy/http/HttpConfig.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -92,6 +93,37 @@ class HttpStatusCodeList HttpStatusBitset _data; }; +/** + * Pre-parsed list of targeted cache control header names (RFC 9213). + * + * Instead of parsing a comma-separated string on each request, this class + * stores the header names as an array of string_views into a stable backing + * string. The Converter ensures the string is parsed once at config load time + * and whenever the per-transaction override is set. + */ +class TargetedCacheControlHeaders +{ +public: + static const MgmtConverter Conv; + + /// Maximum number of targeted headers supported. + static constexpr size_t MAX_HEADERS = 8; + + char *conf_value{nullptr}; + std::string_view headers[MAX_HEADERS]; + size_t count{0}; + + /// Parse a comma-separated header list into the headers array. + void parse(std::string_view src); + + /// Return a span of the parsed headers. + std::span + get_headers() const + { + return std::span{headers, count}; + } +}; + struct HttpStatsBlock { // Need two stats for these for counts and times Metrics::Counter::AtomicType *background_fill_bytes_aborted; @@ -550,8 +582,7 @@ struct OverridableHttpConfigParams { MgmtByte cache_range_write = 0; MgmtByte allow_multi_range = 0; - char *targeted_cache_control_headers = nullptr; // This does not get free'd by us! - size_t targeted_cache_control_headers_len = 0; // Updated when targeted headers are set. + TargetedCacheControlHeaders targeted_cache_control_headers; MgmtByte ignore_accept_mismatch = 0; MgmtByte ignore_accept_language_mismatch = 0; @@ -887,7 +918,9 @@ class ParsedConfigCache */ struct ParsedValue { std::string conf_value_storage{}; // Owns the string data. - std::variant parsed{}; + std::variant + parsed{}; }; /** Return the parsed value for the configuration. @@ -988,6 +1021,7 @@ inline HttpConfigParams::~HttpConfigParams() ats_free(oride.host_res_data.conf_value); ats_free(oride.negative_caching_list.conf_value); ats_free(oride.negative_revalidating_list.conf_value); + ats_free(oride.targeted_cache_control_headers.conf_value); delete connect_ports; delete redirect_actions_map; diff --git a/include/proxy/http/OverridableConfigDefs.h b/include/proxy/http/OverridableConfigDefs.h index 9e29bcffb50..d70c4c54caa 100644 --- a/include/proxy/http/OverridableConfigDefs.h +++ b/include/proxy/http/OverridableConfigDefs.h @@ -250,6 +250,6 @@ X(HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE, connect_attempts_retry_backoff_base, "proxy.config.http.connect_attempts_retry_backoff_base", INT, GENERIC) \ X(HTTP_NEGATIVE_REVALIDATING_LIST, negative_revalidating_list, "proxy.config.http.negative_revalidating_list", STRING, HttpStatusCodeList_Conv) \ X(HTTP_CACHE_POST_METHOD, cache_post_method, "proxy.config.http.cache.post_method", INT, GENERIC) \ - X(HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, targeted_cache_control_headers, "proxy.config.http.cache.targeted_cache_control_headers", STRING, GENERIC) + X(HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, targeted_cache_control_headers, "proxy.config.http.cache.targeted_cache_control_headers", STRING, TargetedCacheControlHeaders_Conv) // clang-format on diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index d9628ed1da9..bccbba6f9ac 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7298,6 +7298,10 @@ _memberp_to_generic(MgmtFloat *ptr, MgmtConverter const *&conv) -> typename std: #define _CONF_CASE_HttpTransact_HOST_RES_CONV(KEY, MEMBER) \ case TS_CONFIG_##KEY: ret = &overridableHttpConfig->MEMBER; conv = &HttpTransact::HOST_RES_CONV; break; +// Custom converter: Parses/formats targeted cache control header lists. +#define _CONF_CASE_TargetedCacheControlHeaders_Conv(KEY, MEMBER) \ + case TS_CONFIG_##KEY: ret = &overridableHttpConfig->MEMBER; conv = &TargetedCacheControlHeaders::Conv; break; + // Dispatcher: Routes to _CONF_CASE_ based on the CONV parameter. #define _CONF_CASE_DISPATCH(KEY, MEMBER, RECORD_NAME, DATA_TYPE, CONV) _CONF_CASE_##CONV(KEY, MEMBER) @@ -7348,6 +7352,7 @@ _conf_to_memberp(TSOverridableConfigKey conf, OverridableHttpConfigParams *overr #undef _CONF_CASE_ConnectionTracker_MAX_SERVER_CONV #undef _CONF_CASE_ConnectionTracker_SERVER_MATCH_CONV #undef _CONF_CASE_HttpTransact_HOST_RES_CONV +#undef _CONF_CASE_TargetedCacheControlHeaders_Conv #undef _CONF_CASE_DISPATCH // 2nd little helper function to find the struct member for getting. @@ -7563,11 +7568,10 @@ TSHttpTxnConfigStringSet(TSHttpTxn txnp, TSOverridableConfigKey conf, const char break; case TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS: if (value && length > 0) { - s->t_state.my_txn_conf().targeted_cache_control_headers = const_cast(value); - s->t_state.my_txn_conf().targeted_cache_control_headers_len = length; + auto &parsed = ParsedConfigCache::lookup(conf, std::string_view(value, length)); + s->t_state.my_txn_conf().targeted_cache_control_headers = std::get(parsed.parsed); } else { - s->t_state.my_txn_conf().targeted_cache_control_headers = nullptr; - s->t_state.my_txn_conf().targeted_cache_control_headers_len = 0; + s->t_state.my_txn_conf().targeted_cache_control_headers = TargetedCacheControlHeaders{}; } break; default: { diff --git a/src/proxy/hdrs/HdrToken.cc b/src/proxy/hdrs/HdrToken.cc index 6e5d44cf116..712a5ed7f73 100644 --- a/src/proxy/hdrs/HdrToken.cc +++ b/src/proxy/hdrs/HdrToken.cc @@ -125,7 +125,10 @@ const char *const _hdrtoken_strs[] = { "br", // RFC-8878 - "zstd"}; + "zstd", + + // RFC-9213 Targeted Cache Control + "CDN-Cache-Control"}; HdrTokenTypeBinding _hdrtoken_strs_type_initializers[] = { {"file", HdrTokenType::SCHEME }, @@ -267,6 +270,7 @@ HdrTokenFieldInfo _hdrtoken_strs_field_initializers[] = { {"Forwarded", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, (HdrTokenInfoFlags::COMMAS | HdrTokenInfoFlags::MULTVALS) }, {"Sec-WebSocket-Key", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, HdrTokenInfoFlags::NONE }, {"Sec-WebSocket-Version", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, HdrTokenInfoFlags::NONE }, + {"CDN-Cache-Control", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, (HdrTokenInfoFlags::COMMAS | HdrTokenInfoFlags::MULTVALS) }, {nullptr, 0, 0, HdrTokenInfoFlags::NONE }, }; diff --git a/src/proxy/hdrs/MIME.cc b/src/proxy/hdrs/MIME.cc index f2fa4fbb52b..0b294890f5f 100644 --- a/src/proxy/hdrs/MIME.cc +++ b/src/proxy/hdrs/MIME.cc @@ -3713,7 +3713,7 @@ MIMEHdrImpl::recompute_accelerators_and_presence_bits() //////////////////////////////////////////////////////// void -MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null, const char *targeted_headers_str) +MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null, std::span targeted_headers) { int len, tlen; const char *s; @@ -3734,17 +3734,10 @@ MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null, const cha field = nullptr; // Check for targeted cache control headers first (in priority order). - if (targeted_headers_str && *targeted_headers_str) { - swoc::TextView config_view{targeted_headers_str}; - while (config_view) { - swoc::TextView header_name = config_view.take_prefix_at(',').trim_if(&isspace); - if (!header_name.empty()) { - field = mime_hdr_field_find(this, std::string_view{header_name.data(), header_name.size()}); - if (field) { - // Found a targeted header, use it and stop searching. - break; - } - } + for (const auto &header_name : targeted_headers) { + field = mime_hdr_field_find(this, header_name); + if (field) { + break; } } diff --git a/src/proxy/http/HttpConfig.cc b/src/proxy/http/HttpConfig.cc index 63ed5e45668..92e59917f5b 100644 --- a/src/proxy/http/HttpConfig.cc +++ b/src/proxy/http/HttpConfig.cc @@ -694,6 +694,40 @@ const MgmtConverter HttpStatusCodeList::Conv{ }}; // clang-format on +///////////////////////////////////////////////////////////// +// +// TargetedCacheControlHeaders implementation +// +///////////////////////////////////////////////////////////// + +void +TargetedCacheControlHeaders::parse(std::string_view src) +{ + count = 0; + swoc::TextView config_view{src}; + + while (config_view && count < MAX_HEADERS) { + swoc::TextView header_name = config_view.take_prefix_at(',').trim_if(&isspace); + if (!header_name.empty()) { + headers[count++] = std::string_view{header_name.data(), header_name.size()}; + } + } +} + +// clang-format off +const MgmtConverter TargetedCacheControlHeaders::Conv{ + [](const void *data) -> std::string_view { + const TargetedCacheControlHeaders *hdrs = static_cast(data); + return hdrs->conf_value ? hdrs->conf_value : ""; + }, + [](void *data, std::string_view src) -> void { + TargetedCacheControlHeaders *hdrs = static_cast(data); + // The string_views in headers[] point into conf_value, so conf_value must + // remain stable for the lifetime of the parsed headers. + hdrs->parse(src); + }}; +// clang-format on + ///////////////////////////////////////////////////////////// // // ParsedConfigCache implementation @@ -784,6 +818,14 @@ ParsedConfigCache::parse(TSOverridableConfigKey key, std::string_view value) break; } + case TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS: { + TargetedCacheControlHeaders targeted_headers{}; + targeted_headers.conf_value = const_cast(result.conf_value_storage.data()); + targeted_headers.parse(result.conf_value_storage); + result.parsed = targeted_headers; + break; + } + default: // No special parsing needed for this config. break; @@ -1093,10 +1135,11 @@ HttpConfig::startup() HttpEstablishStaticConfigByte(c.oride.cache_required_headers, "proxy.config.http.cache.required_headers"); HttpEstablishStaticConfigByte(c.oride.cache_range_lookup, "proxy.config.http.cache.range.lookup"); HttpEstablishStaticConfigByte(c.oride.cache_range_write, "proxy.config.http.cache.range.write"); - HttpEstablishStaticConfigStringAlloc(c.oride.targeted_cache_control_headers, + HttpEstablishStaticConfigStringAlloc(c.oride.targeted_cache_control_headers.conf_value, "proxy.config.http.cache.targeted_cache_control_headers"); - c.oride.targeted_cache_control_headers_len = - c.oride.targeted_cache_control_headers ? strlen(c.oride.targeted_cache_control_headers) : 0; + if (c.oride.targeted_cache_control_headers.conf_value) { + c.oride.targeted_cache_control_headers.parse(c.oride.targeted_cache_control_headers.conf_value); + } HttpEstablishStaticConfigStringAlloc(c.connect_ports_string, "proxy.config.http.connect_ports"); @@ -1402,12 +1445,13 @@ HttpConfig::reconfigure() params->max_payload_iobuf_index = m_master.max_payload_iobuf_index; params->max_msg_iobuf_index = m_master.max_msg_iobuf_index; - params->oride.cache_required_headers = m_master.oride.cache_required_headers; - params->oride.cache_range_lookup = INT_TO_BOOL(m_master.oride.cache_range_lookup); - params->oride.cache_range_write = INT_TO_BOOL(m_master.oride.cache_range_write); - params->oride.targeted_cache_control_headers = ats_strdup(m_master.oride.targeted_cache_control_headers); - params->oride.targeted_cache_control_headers_len = - params->oride.targeted_cache_control_headers ? strlen(params->oride.targeted_cache_control_headers) : 0; + params->oride.cache_required_headers = m_master.oride.cache_required_headers; + params->oride.cache_range_lookup = INT_TO_BOOL(m_master.oride.cache_range_lookup); + params->oride.cache_range_write = INT_TO_BOOL(m_master.oride.cache_range_write); + params->oride.targeted_cache_control_headers.conf_value = ats_strdup(m_master.oride.targeted_cache_control_headers.conf_value); + if (params->oride.targeted_cache_control_headers.conf_value) { + params->oride.targeted_cache_control_headers.parse(params->oride.targeted_cache_control_headers.conf_value); + } params->oride.allow_multi_range = m_master.oride.allow_multi_range; params->connect_ports_string = ats_strdup(m_master.connect_ports_string); diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index 135601bf3e2..7541f28a9aa 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -2107,12 +2107,9 @@ HttpSM::state_read_server_response_header(int event, void *data) break; } - // Recompute cooked cache control with targeted headers (pass nullptr if not configured). - const char *targeted_headers = - (t_state.txn_conf->targeted_cache_control_headers && t_state.txn_conf->targeted_cache_control_headers[0] != '\0') ? - t_state.txn_conf->targeted_cache_control_headers : - nullptr; - t_state.hdr_info.server_response.m_mime->recompute_cooked_stuff(nullptr, targeted_headers); + // Recompute cooked cache control with targeted headers. + t_state.hdr_info.server_response.m_mime->recompute_cooked_stuff(nullptr, + t_state.txn_conf->targeted_cache_control_headers.get_headers()); SMDbg(dbg_ctl_http_seq, "Done parsing server response header"); From cb13f3de3ea25534c6eddfaebef83f2189f608ea Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Sat, 7 Feb 2026 00:03:23 +0000 Subject: [PATCH 3/3] avoid for old compilers --- include/proxy/hdrs/MIME.h | 4 ++-- include/proxy/http/HttpConfig.h | 14 ++++++++++---- src/proxy/hdrs/MIME.cc | 7 ++++--- src/proxy/http/HttpSM.cc | 3 ++- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/include/proxy/hdrs/MIME.h b/include/proxy/hdrs/MIME.h index 555f540872c..7605fc0640b 100644 --- a/include/proxy/hdrs/MIME.h +++ b/include/proxy/hdrs/MIME.h @@ -24,7 +24,6 @@ #pragma once #include -#include #include #include @@ -326,7 +325,8 @@ struct MIMEHdrImpl : public HdrHeapObjImpl { void check_strings(HeapCheck *heaps, int num_heaps); // Cooked values - void recompute_cooked_stuff(MIMEField *changing_field_or_null = nullptr, std::span targeted_headers = {}); + void recompute_cooked_stuff(MIMEField *changing_field_or_null = nullptr, const std::string_view *targeted_headers = nullptr, + size_t targeted_headers_count = 0); void recompute_accelerators_and_presence_bits(); // Utility diff --git a/include/proxy/http/HttpConfig.h b/include/proxy/http/HttpConfig.h index 8580fa3563c..7b31f1ba6dd 100644 --- a/include/proxy/http/HttpConfig.h +++ b/include/proxy/http/HttpConfig.h @@ -39,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -116,11 +115,18 @@ class TargetedCacheControlHeaders /// Parse a comma-separated header list into the headers array. void parse(std::string_view src); - /// Return a span of the parsed headers. - std::span + /// Return a pointer to the parsed headers array. + const std::string_view * get_headers() const { - return std::span{headers, count}; + return headers; + } + + /// Return the number of parsed headers. + size_t + get_count() const + { + return count; } }; diff --git a/src/proxy/hdrs/MIME.cc b/src/proxy/hdrs/MIME.cc index 0b294890f5f..32045d1abbc 100644 --- a/src/proxy/hdrs/MIME.cc +++ b/src/proxy/hdrs/MIME.cc @@ -3713,7 +3713,8 @@ MIMEHdrImpl::recompute_accelerators_and_presence_bits() //////////////////////////////////////////////////////// void -MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null, std::span targeted_headers) +MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null, const std::string_view *targeted_headers, + size_t targeted_headers_count) { int len, tlen; const char *s; @@ -3734,8 +3735,8 @@ MIMEHdrImpl::recompute_cooked_stuff(MIMEField *changing_field_or_null, std::span field = nullptr; // Check for targeted cache control headers first (in priority order). - for (const auto &header_name : targeted_headers) { - field = mime_hdr_field_find(this, header_name); + for (size_t i = 0; i < targeted_headers_count; ++i) { + field = mime_hdr_field_find(this, targeted_headers[i]); if (field) { break; } diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index 7541f28a9aa..bded71cf011 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -2109,7 +2109,8 @@ HttpSM::state_read_server_response_header(int event, void *data) // Recompute cooked cache control with targeted headers. t_state.hdr_info.server_response.m_mime->recompute_cooked_stuff(nullptr, - t_state.txn_conf->targeted_cache_control_headers.get_headers()); + t_state.txn_conf->targeted_cache_control_headers.get_headers(), + t_state.txn_conf->targeted_cache_control_headers.get_count()); SMDbg(dbg_ctl_http_seq, "Done parsing server response header");