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 d1a722fc823..b8697d5c999 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -2479,6 +2479,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 0df84ed3c9b..fac66af7dda 100644 --- a/doc/admin-guide/plugins/lua.en.rst +++ b/doc/admin-guide/plugins/lua.en.rst @@ -4394,6 +4394,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 dcea4893594..1c75fe88541 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 bfdef446c67..a582cdcbf83 100644 --- a/include/proxy/http/HttpConfig.h +++ b/include/proxy/http/HttpConfig.h @@ -545,6 +545,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/ts/apidefs.h.in b/include/ts/apidefs.h.in index 0bc28141907..620d7611cde 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/plugins/lua/ts_lua_http_config.cc b/plugins/lua/ts_lua_http_config.cc index 1543cf25092..8d45ecd749b 100644 --- a/plugins/lua/ts_lua_http_config.cc +++ b/plugins/lua/ts_lua_http_config.cc @@ -154,6 +154,7 @@ typedef enum { TS_LUA_CONFIG_HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE = TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE, TS_LUA_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIST = TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIST, TS_LUA_CONFIG_HTTP_CACHE_POST_METHOD = TS_CONFIG_HTTP_CACHE_POST_METHOD, + TS_LUA_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS = TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, TS_LUA_CONFIG_LAST_ENTRY = TS_CONFIG_LAST_ENTRY, } TSLuaOverridableConfigKey; @@ -299,6 +300,7 @@ ts_lua_var_item ts_lua_http_config_vars[] = { TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE), TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIST), TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_CACHE_POST_METHOD), + TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS), TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_LAST_ENTRY), }; diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index f1869503f02..5063040a7ad 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7462,6 +7462,9 @@ _conf_to_memberp(TSOverridableConfigKey conf, OverridableHttpConfigParams *overr case TS_CONFIG_HTTP_CACHE_POST_METHOD: ret = _memberp_to_generic(&overridableHttpConfig->cache_post_method, conv); break; + case TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS: + ret = _memberp_to_generic(&overridableHttpConfig->targeted_cache_control_headers, conv); + break; case TS_CONFIG_HTTP_REQUEST_BUFFER_ENABLED: ret = _memberp_to_generic(&overridableHttpConfig->request_buffer_enabled, conv); break; @@ -7748,6 +7751,15 @@ TSHttpTxnConfigStringSet(TSHttpTxn txnp, TSOverridableConfigKey conf, const char s->t_state.my_txn_conf().global_user_agent_header_size = 0; } 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); // The "core" likes non-const char* + 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; case TS_CONFIG_BODY_FACTORY_TEMPLATE_BASE: if (value && length > 0) { s->t_state.my_txn_conf().body_factory_template_base = const_cast(value); @@ -7860,6 +7872,10 @@ TSHttpTxnConfigStringGet(TSHttpTxn txnp, TSOverridableConfigKey conf, const char *value = sm->t_state.txn_conf->global_user_agent_header; *length = sm->t_state.txn_conf->global_user_agent_header_size; break; + case TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS: + *value = sm->t_state.txn_conf->targeted_cache_control_headers; + *length = sm->t_state.txn_conf->targeted_cache_control_headers_len; + break; case TS_CONFIG_BODY_FACTORY_TEMPLATE_BASE: *value = sm->t_state.txn_conf->body_factory_template_base; *length = sm->t_state.txn_conf->body_factory_template_base_len; diff --git a/src/api/InkAPITest.cc b/src/api/InkAPITest.cc index 21d80fc57f3..59dfa16fc6b 100644 --- a/src/api/InkAPITest.cc +++ b/src/api/InkAPITest.cc @@ -8827,6 +8827,7 @@ std::array SDK_Overridable_Configs = { "proxy.config.http.connect_attempts_retry_backoff_base", "proxy.config.http.negative_revalidating_list", "proxy.config.http.cache.post_method", + "proxy.config.http.cache.targeted_cache_control_headers", } }; // clang-format on diff --git a/src/proxy/hdrs/MIME.cc b/src/proxy/hdrs/MIME.cc index fe7365a16b4..b90925fd591 100644 --- a/src/proxy/hdrs/MIME.cc +++ b/src/proxy/hdrs/MIME.cc @@ -3711,7 +3711,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; @@ -3723,13 +3723,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 adead9915db..33113d8357d 100644 --- a/src/proxy/http/HttpConfig.cc +++ b/src/proxy/http/HttpConfig.cc @@ -995,6 +995,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"); @@ -1300,10 +1304,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 fb306a7f6b3..4cc65603475 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -2059,14 +2059,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 @@ -2097,6 +2103,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 396a8d8d2e3..f002d8943a7 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -638,6 +638,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/src/shared/overridable_txn_vars.cc b/src/shared/overridable_txn_vars.cc index 6550387ca8f..55721f9bad7 100644 --- a/src/shared/overridable_txn_vars.cc +++ b/src/shared/overridable_txn_vars.cc @@ -137,6 +137,8 @@ const std::unordered_map