From b8aa0d3fae1fd3ee7074c54234de625a32d9b9fe Mon Sep 17 00:00:00 2001 From: Juan Posadas Date: Wed, 22 Jul 2026 16:41:10 -0600 Subject: [PATCH 1/4] header_rewrite: recognize POST_REMAP_HOOK hook condition Add the POST_REMAP_HOOK keyword to the config parser so rulesets can target the real post-remap hook. Parser-only; runtime wiring follows. --- plugins/header_rewrite/header_rewrite_test.cc | 13 +++++++++++++ plugins/header_rewrite/parser.cc | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/plugins/header_rewrite/header_rewrite_test.cc b/plugins/header_rewrite/header_rewrite_test.cc index 6f6026e3df3..4f4c69c75ed 100644 --- a/plugins/header_rewrite/header_rewrite_test.cc +++ b/plugins/header_rewrite/header_rewrite_test.cc @@ -126,6 +126,19 @@ test_parsing() END_TEST(); } + { + ParserTest p("cond %{POST_REMAP_HOOK}"); + TSHttpHookID hook = TS_HTTP_LAST_HOOK; + + CHECK_EQ(p.getTokens().size(), 2U); + CHECK_EQ(p.getTokens()[0], "cond"); + CHECK_EQ(p.getTokens()[1], "%{POST_REMAP_HOOK}"); + CHECK_EQ(p.cond_is_hook(hook), true); + CHECK_EQ(hook, TS_HTTP_POST_REMAP_HOOK); + + END_TEST(); + } + { ParserTest p("cond %{CLIENT-HEADER:Host} =a"); diff --git a/plugins/header_rewrite/parser.cc b/plugins/header_rewrite/parser.cc index 745f7d0882e..c1f467d39a8 100644 --- a/plugins/header_rewrite/parser.cc +++ b/plugins/header_rewrite/parser.cc @@ -296,6 +296,10 @@ Parser::cond_is_hook(TSHttpHookID &hook) const hook = TS_REMAP_PSEUDO_HOOK; return true; } + if ("POST_REMAP_HOOK" == _op) { + hook = TS_HTTP_POST_REMAP_HOOK; + return true; + } if ("TXN_START_HOOK" == _op) { hook = TS_HTTP_TXN_START_HOOK; return true; From a77da68d3b0335d8b5f6ce5b919a2448437966db Mon Sep 17 00:00:00 2001 From: Juan Posadas Date: Wed, 22 Jul 2026 16:52:31 -0600 Subject: [PATCH 2/4] header_rewrite: run rulesets on TS_HTTP_POST_REMAP_HOOK Map the POST_REMAP event in the continuation, gather the post-remap request headers, and allow operators/conditions on the hook. Enables the global-plugin deployment model where rules act after remapping. Adds an end-to-end autest and documents the hook. --- doc/admin-guide/plugins/header_rewrite.en.rst | 13 ++++ plugins/header_rewrite/header_rewrite.cc | 3 + plugins/header_rewrite/resources.cc | 3 +- plugins/header_rewrite/statement.cc | 1 + .../header_rewrite_post_remap.replay.yaml | 76 +++++++++++++++++++ .../header_rewrite_post_remap.test.py | 24 ++++++ .../pluginTest/header_rewrite/post_remap.conf | 22 ++++++ 7 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml create mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py create mode 100644 tests/gold_tests/pluginTest/header_rewrite/post_remap.conf diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index 2d57f64f973..ac5916f82a5 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -1734,6 +1734,19 @@ files shared by both the global :file:`plugin.config` and individual remapping entries in :file:`remap.config`, this hook condition will force the subsequent ruleset(s) to be valid only for remapped transactions. +POST_REMAP_HOOK +~~~~~~~~~~~~~~~ + +Forces evaluation of the ruleset immediately after remapping has completed, but +before |TS| contacts the origin server (or fetches the object from cache). There +is no response data yet, so context-adapting conditions and operators match +against the request. + +This hook is available to both global (:file:`plugin.config`) and per-remap +(:file:`remap.config`) configurations, and is primarily useful for +globally-configured ``header_rewrite`` instances that need to act on the request +after remapping. + SEND_REQUEST_HDR_HOOK ~~~~~~~~~~~~~~~~~~~~~ diff --git a/plugins/header_rewrite/header_rewrite.cc b/plugins/header_rewrite/header_rewrite.cc index 9a43a73813b..b8c4f3c6dbf 100644 --- a/plugins/header_rewrite/header_rewrite.cc +++ b/plugins/header_rewrite/header_rewrite.cc @@ -494,6 +494,9 @@ cont_rewrite_headers(TSCont contp, TSEvent event, void *edata) case TS_EVENT_HTTP_READ_REQUEST_PRE_REMAP: hook = TS_HTTP_PRE_REMAP_HOOK; break; + case TS_EVENT_HTTP_POST_REMAP: + hook = TS_HTTP_POST_REMAP_HOOK; + break; case TS_EVENT_HTTP_SEND_REQUEST_HDR: hook = TS_HTTP_SEND_REQUEST_HDR_HOOK; break; diff --git a/plugins/header_rewrite/resources.cc b/plugins/header_rewrite/resources.cc index ed8d4ffe6a9..5ccabde62f5 100644 --- a/plugins/header_rewrite/resources.cc +++ b/plugins/header_rewrite/resources.cc @@ -87,7 +87,8 @@ Resources::gather(const ResourceIDs ids, TSHttpHookID hook) case TS_HTTP_READ_REQUEST_HDR_HOOK: case TS_HTTP_PRE_REMAP_HOOK: - // Read request from client + case TS_HTTP_POST_REMAP_HOOK: + // Read request from client (post-remap this is the remapped request) if (ids & RSRC_CLIENT_REQUEST_HEADERS) { bufp = client_bufp; hdr_loc = client_hdr_loc; diff --git a/plugins/header_rewrite/statement.cc b/plugins/header_rewrite/statement.cc index 351f5d572e3..0c699af09ca 100644 --- a/plugins/header_rewrite/statement.cc +++ b/plugins/header_rewrite/statement.cc @@ -81,6 +81,7 @@ Statement::initialize_hooks() add_allowed_hook(TS_HTTP_SEND_REQUEST_HDR_HOOK); add_allowed_hook(TS_HTTP_SEND_RESPONSE_HDR_HOOK); add_allowed_hook(TS_REMAP_PSEUDO_HOOK); + add_allowed_hook(TS_HTTP_POST_REMAP_HOOK); add_allowed_hook(TS_HTTP_TXN_START_HOOK); add_allowed_hook(TS_HTTP_TXN_CLOSE_HOOK); } diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml new file mode 100644 index 00000000000..b39d5a4bbed --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml @@ -0,0 +1,76 @@ +# 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. + +meta: + version: "1.0" + +autest: + description: 'Test header_rewrite POST_REMAP_HOOK support (global plugin)' + + dns: + name: 'dns' + + server: + name: 'server' + + client: + name: 'client' + + ats: + name: 'ts' + + copy_to_config_dir: + - 'post_remap.conf' + + records_config: + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: 'header_rewrite' + + # header_rewrite loaded as a GLOBAL plugin. The conf resolves relative to + # the ATS config dir, where copy_to_config_dir places it. + plugin_config: + - 'header_rewrite.so post_remap.conf' + + remap_config: + - from: "http://www.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + +sessions: +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /post_remap/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 1 ] + + # The header set at POST_REMAP must be present on the request ATS forwards. + proxy-request: + headers: + fields: + - [ X-Post-Remap-Applied, { value: "yes", as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py new file mode 100644 index 00000000000..3ea824b4b1e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py @@ -0,0 +1,24 @@ +''' +Test header_rewrite POST_REMAP_HOOK support. +''' +# 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 header_rewrite attaching a ruleset to the POST_REMAP_HOOK. +''' + +Test.ATSReplayTest(replay_file="header_rewrite_post_remap.replay.yaml",) diff --git a/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf b/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf new file mode 100644 index 00000000000..22680113627 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf @@ -0,0 +1,22 @@ +# +# 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. + +# Global header_rewrite ruleset that fires after remapping. It sets a request +# header on the post-remap request, which must then appear on the request ATS +# forwards to the origin (proxy-request). +cond %{POST_REMAP_HOOK} + set-header X-Post-Remap-Applied "yes" From 56eccda7876ff5b10a1219f28a69277df379ee76 Mon Sep 17 00:00:00 2001 From: Juan Posadas Date: Mon, 27 Jul 2026 13:18:41 -0600 Subject: [PATCH 3/4] doc: clarify why POST_REMAP_HOOK exists The earlier wording sold the hook as the window before the origin request, which SEND_REQUEST_HDR_HOOK already covers. The window that matters is before the cache lookup, and the gap is specific to global (plugin.config) rulesets: the read-request hooks run pre-remap, SEND_REQUEST_HDR_HOOK runs after the lookup and only on a forward to origin, and REMAP_PSEUDO_HOOK closes the window for remap.config only. --- doc/admin-guide/plugins/header_rewrite.en.rst | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index ac5916f82a5..7ba3fd6d223 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -1738,14 +1738,17 @@ POST_REMAP_HOOK ~~~~~~~~~~~~~~~ Forces evaluation of the ruleset immediately after remapping has completed, but -before |TS| contacts the origin server (or fetches the object from cache). There -is no response data yet, so context-adapting conditions and operators match -against the request. - -This hook is available to both global (:file:`plugin.config`) and per-remap -(:file:`remap.config`) configurations, and is primarily useful for -globally-configured ``header_rewrite`` instances that need to act on the request -after remapping. +before |TS| looks the request up in the cache. There is no response data yet, so +context-adapting conditions and operators match against the request, which at +this point is the remapped request. + +For rulesets in :file:`remap.config`, `REMAP_PSEUDO_HOOK`_ already covers this +window. This hook exists for globally-configured rulesets, which otherwise have +no hook that sees the remapped request before the cache lookup: +`READ_REQUEST_HDR_HOOK`_ and `READ_REQUEST_PRE_REMAP_HOOK`_ run before +remapping, and `SEND_REQUEST_HDR_HOOK`_ runs after the lookup, only when the +request is forwarded to an origin. Anything that has to influence the lookup +itself belongs at this hook. SEND_REQUEST_HDR_HOOK ~~~~~~~~~~~~~~~~~~~~~ From c4543973c31b53cbc5fbd63be870dfb217de60d2 Mon Sep 17 00:00:00 2001 From: Juan Posadas Date: Mon, 27 Jul 2026 15:46:32 -0600 Subject: [PATCH 4/4] Make the POST_REMAP autest prove the hook is necessary The old test set a header at POST_REMAP and asserted it on the request forwarded to origin. Swapping the rule to SEND_REQUEST_HDR_HOOK passed just as well, so the test demonstrated nothing that existing hooks couldn't already do. The rule now records the remapped host and echoes it into the client response, and the replay adds a cache-hit transaction. That combination pins both properties the hook uniquely provides: - Fires on a cache hit, where no request goes to origin, so SEND_REQUEST_HDR_HOOK never runs. - Sees the remapped request, so the pre-remap hooks record the wrong host. Verified by rerunning the test with the rule moved to SEND_REQUEST_HDR_HOOK, READ_REQUEST_HDR_HOOK, and READ_REQUEST_PRE_REMAP_HOOK; each one fails. Note %{CLIENT-URL} is the pristine URL by design, so the rule uses %{URL:HOST} to read the in-flight request. --- .../header_rewrite_post_remap.replay.yaml | 55 +++++++++++++++++-- .../pluginTest/header_rewrite/post_remap.conf | 13 +++-- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml index b39d5a4bbed..d3cf64c259d 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml @@ -32,6 +32,9 @@ autest: ats: name: 'ts' + process_config: + enable_cache: true + copy_to_config_dir: - 'post_remap.conf' @@ -43,6 +46,7 @@ autest: # the ATS config dir, where copy_to_config_dir places it. plugin_config: - 'header_rewrite.so post_remap.conf' + - 'xdebug.so --enable=x-cache' remap_config: - from: "http://www.example.com/" @@ -50,6 +54,11 @@ autest: sessions: - transactions: + + ############################################################################# + # Cache miss: the header set at POST_REMAP reaches the origin, and the echo + # rule reports it on the response. + ############################################################################# - client-request: method: "GET" version: "1.1" @@ -57,20 +66,58 @@ sessions: headers: fields: - [ Host, www.example.com ] - - [ uuid, 1 ] + - [ x-debug, "x-cache" ] + - [ uuid, post-remap-miss ] - # The header set at POST_REMAP must be present on the request ATS forwards. proxy-request: headers: fields: - - [ X-Post-Remap-Applied, { value: "yes", as: equal } ] + - [ X-Post-Remap-Host, { value: "backend.ex", as: equal } ] server-response: status: 200 reason: OK headers: fields: - - [ Connection, close ] + - [ Content-Type, text/plain ] + - [ Content-Length, "3" ] + - [ Cache-Control, "max-age=300" ] + content: + encoding: plain + data: xxx proxy-response: status: 200 + headers: + fields: + - [ X-Cache, { value: "miss", as: equal } ] + - [ X-Post-Remap-Echo, { value: "backend.ex", as: equal } ] + + ############################################################################# + # Cache hit: nothing is forwarded to the origin, so SEND_REQUEST_HDR_HOOK + # never runs. The rule still fires, because POST_REMAP is before the lookup. + ############################################################################# + - client-request: + delay: 100ms + method: "GET" + version: "1.1" + url: /post_remap/ + headers: + fields: + - [ Host, www.example.com ] + - [ x-debug, "x-cache" ] + - [ uuid, post-remap-hit ] + + proxy-request: + expect: absent + + server-response: + status: 404 + reason: Not Found + + proxy-response: + status: 200 + headers: + fields: + - [ X-Cache, { value: "hit-fresh", as: equal } ] + - [ X-Post-Remap-Echo, { value: "backend.ex", as: equal } ] diff --git a/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf b/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf index 22680113627..2788617372b 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf +++ b/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf @@ -15,8 +15,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Global header_rewrite ruleset that fires after remapping. It sets a request -# header on the post-remap request, which must then appear on the request ATS -# forwards to the origin (proxy-request). +# Global header_rewrite ruleset that fires after remapping, on the remapped +# request, before the cache lookup. The value is the remapped host, so an +# earlier hook would record the pristine host instead. cond %{POST_REMAP_HOOK} - set-header X-Post-Remap-Applied "yes" + set-header X-Post-Remap-Host "%{URL:HOST}" + +# Echo the post-remap header into the client response so the rule above can be +# observed on a cache hit, where no request is forwarded to the origin. +cond %{SEND_RESPONSE_HDR_HOOK} + set-header X-Post-Remap-Echo "%{CLIENT-HEADER:X-Post-Remap-Host}"