Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions doc/admin-guide/configuration/cache-basics.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://httpwg.org/specs/rfc9213.html>`_
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
-------------------------

Expand Down
25 changes: 25 additions & 0 deletions doc/admin-guide/files/records.yaml.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://httpwg.org/specs/rfc9213.html>`_ 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:
Expand Down
1 change: 1 addition & 0 deletions doc/admin-guide/plugins/lua.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <admin-plugins-ts-lua>`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion include/proxy/hdrs/MIME.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions include/proxy/http/HttpConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions include/ts/apidefs.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
2 changes: 2 additions & 0 deletions plugins/lua/ts_lua_http_config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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),
};

Expand Down
16 changes: 16 additions & 0 deletions src/api/InkAPI.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<char *>(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<char *>(value);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/api/InkAPITest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8827,6 +8827,7 @@ std::array<std::string_view, TS_CONFIG_LAST_ENTRY> 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
Expand Down
30 changes: 25 additions & 5 deletions src/proxy/hdrs/MIME.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<std::string_view>(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<std::string_view>(MIME_FIELD_CACHE_CONTROL));
}

if (field) {
// try pathpaths first -- unlike most other fastpaths, this one
Expand Down
15 changes: 11 additions & 4 deletions src/proxy/http/HttpConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions src/proxy/http/HttpSM.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Comment on lines +2069 to +2075
Copy link
Contributor Author

@bneradt bneradt Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the other interesting production change (part 2).

This is "recooked" here to avoid intermingling the targeted cache control configuration into the MIME/parser interface. The recompute should be cheap, so I think this is OK. But if wanted, I can look into trying to get the parse to happen initially at parse_resp.

SMDbg(dbg_ctl_http_seq, "Done parsing server response header");

// Now that we know that we have all of the origin server
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/records/RecordsConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Expand Down
2 changes: 2 additions & 0 deletions src/shared/overridable_txn_vars.cc
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ const std::unordered_map<std::string_view, std::tuple<const TSOverridableConfigK
{"proxy.config.http.post.check.content_length.enabled",
{TS_CONFIG_HTTP_POST_CHECK_CONTENT_LENGTH_ENABLED, TS_RECORDDATATYPE_INT} },
{"proxy.config.http.cache.post_method", {TS_CONFIG_HTTP_CACHE_POST_METHOD, TS_RECORDDATATYPE_INT} },
{"proxy.config.http.cache.targeted_cache_control_headers",
{TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, TS_RECORDDATATYPE_STRING} },
{"proxy.config.http.cache.cache_urls_that_look_dynamic",
{TS_CONFIG_HTTP_CACHE_CACHE_URLS_THAT_LOOK_DYNAMIC, TS_RECORDDATATYPE_INT} },
{"proxy.config.http.transaction_no_activity_timeout_in",
Expand Down
Loading