From 700f6bf1f0b7b2275c60e3762332c5156647232c Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 23 Apr 2026 10:05:42 -0700 Subject: [PATCH 1/5] Enable robust set-body origin replacement across hooks and transforms. Implement robust internal-body handling across READ_RESPONSE and SEND_RESPONSE paths, including transform interactions, and consolidate origin set-body coverage into a single replay-driven AuTest matrix with cache-bypass probes and clearer inline test documentation. Made-with: Cursor --- doc/admin-guide/plugins/header_rewrite.en.rst | 19 +- plugins/header_rewrite/operators.cc | 1 + src/api/InkAPI.cc | 3 + src/proxy/http/HttpSM.cc | 38 +- ...header_rewrite_set_body_origin.replay.yaml | 345 ++++++++++++++++++ .../header_rewrite_set_body_origin.test.py | 27 ++ .../rules/rule_set_body_origin_read_resp.conf | 19 + .../rules/rule_set_body_origin_send_resp.conf | 19 + tests/prepare_proxy_verifier.sh | 2 +- 9 files changed, 467 insertions(+), 6 deletions(-) create mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml create mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index 2d57f64f973..98cc6604cf6 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -1177,8 +1177,23 @@ set-body set-body -Sets the body to ````. Can also be used to delete a body with ``""``. This is only useful when overriding the origin status, i.e. -intercepting/pre-empting a request so that you can override the body from the body-factory with your own. +Sets the body to ````. Can also be used to delete a body with ``""``. + +For origin response replacement, ``set-body`` is supported at both +``READ_RESPONSE_HDR_HOOK`` and ``SEND_RESPONSE_HDR_HOOK``. Prefer +``READ_RESPONSE_HDR_HOOK`` when possible so body replacement happens before +response body tunneling starts. + +.. note:: + + When ``set-body`` replaces an origin response body, ATS emits the replacement + through its internal error-body path. ``Content-Type`` defaults to + ``text/html`` unless you override it with ``set-header Content-Type``. + Also, ``set-body ""`` does not suppress an origin response body on this hook; + use a non-empty replacement value when sanitizing origin responses. + The gold tests cover origin replacement for both hooks with and without a + response transform plugin. The no-transform matrix runs with HTTP cache + disabled and includes repeated-URL cache-bypass probes. set-body-from ~~~~~~~~~~~~~ diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc index 20207bd24b8..3e34efbe7ee 100644 --- a/plugins/header_rewrite/operators.cc +++ b/plugins/header_rewrite/operators.cc @@ -813,6 +813,7 @@ void OperatorSetBody::initialize_hooks() { add_allowed_hook(TS_REMAP_PSEUDO_HOOK); + add_allowed_hook(TS_HTTP_READ_RESPONSE_HDR_HOOK); add_allowed_hook(TS_HTTP_SEND_RESPONSE_HDR_HOOK); } diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 2f62c45f755..423d26a043a 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -4948,6 +4948,9 @@ TSHttpTxnErrorBodySet(TSHttpTxn txnp, char *buf, size_t buflength, char *mimetyp s->internal_msg_buffer = buf; s->internal_msg_buffer_size = buf ? buflength : 0; s->internal_msg_buffer_fast_allocator_size = -1; + // TSHttpTxnErrorBodySet() and TSHttpTxnServerRequestBodySet() share the same buffer. + // Switching to an error/response body override must clear the request-body mode. + s->api_server_request_body_set = false; s->internal_msg_buffer_type = mimetype; } diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index c8a6b128dc8..0022cc9ead7 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -1687,9 +1687,23 @@ HttpSM::handle_api_return() switch (t_state.next_action) { case HttpTransact::StateMachineAction_t::TRANSFORM_READ: { - HttpTunnelProducer *p = setup_transfer_from_transform(); - perform_transform_cache_write_action(); - tunnel.tunnel_run(p); + if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { + SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform for internal transfer"); + transform_info.vc = nullptr; + t_state.api_info.cache_untransformed = true; + if (t_state.hdr_info.client_response.valid() == 0 && t_state.hdr_info.transform_response.valid()) { + t_state.hdr_info.client_response.create(HTTPType::RESPONSE); + t_state.hdr_info.client_response.copy(&t_state.hdr_info.transform_response); + } + if (server_entry != nullptr && server_entry->in_tunnel == false) { + release_server_session(); + } + setup_internal_transfer(&HttpSM::tunnel_handler); + } else { + HttpTunnelProducer *p = setup_transfer_from_transform(); + perform_transform_cache_write_action(); + tunnel.tunnel_run(p); + } break; } case HttpTransact::StateMachineAction_t::SERVER_READ: { @@ -1722,6 +1736,14 @@ HttpSM::handle_api_return() } setup_blind_tunnel(true, initial_data); + } else if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { + // A plugin replaced the origin response body via TSHttpTxnErrorBodySet(). + // Serve the synthetic body before entering the response body tunnel. + SMDbg(dbg_ctl_http, "plugin set internal body, using internal transfer instead of server tunnel"); + if (server_entry != nullptr && server_entry->in_tunnel == false) { + release_server_session(); + } + setup_internal_transfer(&HttpSM::tunnel_handler); } else { HttpTunnelProducer *p = setup_server_transfer(); perform_cache_write_action(); @@ -8230,6 +8252,16 @@ HttpSM::set_next_state() case HttpTransact::StateMachineAction_t::SERVER_READ: { t_state.source = HttpTransact::Source_t::HTTP_ORIGIN_SERVER; + if (transform_info.vc && t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { + SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform"); + transform_info.vc = nullptr; + t_state.api_info.cache_untransformed = true; + if (t_state.hdr_info.client_response.valid() == 0 && t_state.hdr_info.transform_response.valid()) { + t_state.hdr_info.client_response.create(HTTPType::RESPONSE); + t_state.hdr_info.client_response.copy(&t_state.hdr_info.transform_response); + } + } + if (transform_info.vc) { ink_assert(t_state.hdr_info.client_response.valid() == 0); ink_assert((t_state.hdr_info.transform_response.valid() ? true : false) == true); diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml new file mode 100644 index 00000000000..d044814209f --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml @@ -0,0 +1,345 @@ +# 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 set-body origin replacement without response transforms' + + dns: + name: 'dns' + + server: + name: 'server' + + client: + name: 'client' + process_config: + other_args: '--thread-limit 1' + + ats: + name: 'ts' + + plugin_config: + - null_transform.so + + copy_to_config_dir: + - 'rules' + + records_config: + proxy.config.http.insert_response_via_str: 0 + proxy.config.http.cache.http: 0 + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: 'http|header_rewrite' + + remap_config: + # Block 1: READ_RESPONSE hook replacement path (no transform plugin). + - from: "http://www.example.com/set_body_read_resp_403/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_read_403/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_read_resp.conf" + + # Block 2: SEND_RESPONSE hook replacement path (no transform plugin). + - from: "http://www.example.com/set_body_send_resp_403/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_send_403/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_send_resp.conf" + + # Block 3a: cache-bypass probe for READ_RESPONSE hook (same URL twice). + - from: "http://www.example.com/cache_probe_read/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/cache_probe_read/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_read_resp.conf" + + # Block 3b: cache-bypass probe for SEND_RESPONSE hook (same URL twice). + - from: "http://www.example.com/cache_probe_send/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/cache_probe_send/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_send_resp.conf" + + # Block 4a: READ_RESPONSE hook with response transform plugin active. + - from: "http://www.example.com/set_body_transform_read/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_transform_read/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_read_resp.conf" + + # Block 4b: SEND_RESPONSE hook with response transform plugin active. + - from: "http://www.example.com/set_body_transform_send/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/origin_transform_send/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_origin_send_resp.conf" + +sessions: + +- transactions: + # Block 1 verification: READ_RESPONSE hook replacement. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_read_resp_403/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 1 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + - [ Content-Type, { value: "text/html", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 2 verification: SEND_RESPONSE hook replacement. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_send_resp_403/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 2 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + - [ Content-Type, { value: "text/html", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3a verification: cache-bypass probe for READ_RESPONSE. + # First response on repeated URL. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_read/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 3 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "5" ] + - [ Content-Type, "text/plain" ] + content: + size: 5 + data: "first" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3a verification: cache-bypass probe for READ_RESPONSE. + # Second response on repeated URL should still be replaced. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_read/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 4 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "6" ] + - [ Content-Type, "text/plain" ] + content: + size: 6 + data: "second" + + proxy-response: + status: 200 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3b verification: cache-bypass probe for SEND_RESPONSE. + # First response on repeated URL. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_send/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 5 ] + + server-response: + status: 403 + reason: Forbidden + headers: + fields: + - [ Content-Length, "5" ] + - [ Content-Type, "text/plain" ] + content: + size: 5 + data: "first" + + proxy-response: + status: 403 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 3b verification: cache-bypass probe for SEND_RESPONSE. + # Second response on repeated URL should still be replaced. + - client-request: + method: "GET" + version: "1.1" + url: /cache_probe_send/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 6 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "6" ] + - [ Content-Type, "text/plain" ] + content: + size: 6 + data: "second" + + proxy-response: + status: 200 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 4a verification: READ_RESPONSE with transform plugin active. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_transform_read/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 7 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 200 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" + + # Block 4b verification: SEND_RESPONSE with transform plugin active. + - client-request: + method: "GET" + version: "1.1" + url: /set_body_transform_send/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 8 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, "40" ] + - [ Content-Type, "text/plain" ] + content: + size: 40 + data: "Sensitive account info: secret-key-12345" + + proxy-response: + status: 200 + headers: + fields: + - [ Content-Length, { value: "9", as: equal } ] + content: + size: 9 + data: "Sanitized" diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py new file mode 100644 index 00000000000..0eb53765b2e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py @@ -0,0 +1,27 @@ +''' +Test header_rewrite set-body replacing origin server response bodies. +''' +# 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 set-body origin replacement matrix: +- no transform plugin (READ_RESPONSE_HDR and SEND_RESPONSE_HDR, with cache-bypass probes) +- null_transform plugin active (READ_RESPONSE_HDR and SEND_RESPONSE_HDR) +''' + +Test.SkipUnless(Condition.PluginExists('null_transform.so')) +Test.ATSReplayTest(replay_file="header_rewrite_set_body_origin.replay.yaml") diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf new file mode 100644 index 00000000000..3d024c2c89e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf @@ -0,0 +1,19 @@ +# +# 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. + +cond %{READ_RESPONSE_HDR_HOOK} + set-body "Sanitized" diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf new file mode 100644 index 00000000000..4d4321dee6e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf @@ -0,0 +1,19 @@ +# +# 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. + +cond %{SEND_RESPONSE_HDR_HOOK} + set-body "Sanitized" diff --git a/tests/prepare_proxy_verifier.sh b/tests/prepare_proxy_verifier.sh index 3e853a30f1b..8c570efc7f8 100755 --- a/tests/prepare_proxy_verifier.sh +++ b/tests/prepare_proxy_verifier.sh @@ -40,7 +40,7 @@ pv_dir="${pv_name}-${pv_version}" pv_tar_filename="${pv_dir}.tar.gz" pv_tar="${pv_top_dir}/${pv_tar_filename}" pv_tar_url="https://ci.trafficserver.apache.org/bintray/${pv_tar_filename}" -expected_sha1="e11b5867a56c5ffd496b18c901f1273e9c120a47" +expected_sha1="0a60c646cbc9326abb2fbc397cb9efa8c08a807a" pv_client="${bin_dir}/verifier-client" pv_server="${bin_dir}/verifier-server" TAR=${TAR:-tar} From 6a79af5cb201610f5fb87a8740b288854643c449 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 23 Apr 2026 12:27:02 -0700 Subject: [PATCH 2/5] Address Copilot review feedback on transform safety and docs. Harden transform bypass cleanup before internal transfer, keep no-transform cache probes transform-free, and clarify set-body empty-string behavior in docs. Made-with: Cursor --- doc/admin-guide/plugins/header_rewrite.en.rst | 5 +++-- src/proxy/http/HttpSM.cc | 15 +++++++++++++-- .../header_rewrite_set_body_origin.replay.yaml | 14 ++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index 98cc6604cf6..d947fc33a7f 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -1189,8 +1189,9 @@ response body tunneling starts. When ``set-body`` replaces an origin response body, ATS emits the replacement through its internal error-body path. ``Content-Type`` defaults to ``text/html`` unless you override it with ``set-header Content-Type``. - Also, ``set-body ""`` does not suppress an origin response body on this hook; - use a non-empty replacement value when sanitizing origin responses. + ``set-body ""`` clears the internal replacement body, but does not suppress an + origin response body on this hook; use a non-empty replacement value when + sanitizing origin responses. The gold tests cover origin replacement for both hooks with and without a response transform plugin. The no-transform matrix runs with HTTP cache disabled and includes repeated-URL cache-bypass probes. diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index 0022cc9ead7..bfddfa6ac01 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -1689,8 +1689,15 @@ HttpSM::handle_api_return() case HttpTransact::StateMachineAction_t::TRANSFORM_READ: { if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform for internal transfer"); - transform_info.vc = nullptr; t_state.api_info.cache_untransformed = true; + if (tunnel.is_tunnel_active()) { + tunnel.kill_tunnel(); + } + if (transform_info.entry != nullptr) { + vc_table.cleanup_entry(transform_info.entry); + transform_info.entry = nullptr; + } + transform_info.vc = nullptr; if (t_state.hdr_info.client_response.valid() == 0 && t_state.hdr_info.transform_response.valid()) { t_state.hdr_info.client_response.create(HTTPType::RESPONSE); t_state.hdr_info.client_response.copy(&t_state.hdr_info.transform_response); @@ -8254,8 +8261,12 @@ HttpSM::set_next_state() if (transform_info.vc && t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform"); - transform_info.vc = nullptr; t_state.api_info.cache_untransformed = true; + if (transform_info.entry != nullptr) { + vc_table.cleanup_entry(transform_info.entry); + transform_info.entry = nullptr; + } + transform_info.vc = nullptr; if (t_state.hdr_info.client_response.valid() == 0 && t_state.hdr_info.transform_response.valid()) { t_state.hdr_info.client_response.create(HTTPType::RESPONSE); t_state.hdr_info.client_response.copy(&t_state.hdr_info.transform_response); diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml index d044814209f..8363f020ef5 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml @@ -193,6 +193,7 @@ sessions: # Block 3a verification: cache-bypass probe for READ_RESPONSE. # Second response on repeated URL should still be replaced. + # Keep this as non-200 so null_transform does not engage. - client-request: method: "GET" version: "1.1" @@ -203,8 +204,8 @@ sessions: - [ uuid, 4 ] server-response: - status: 200 - reason: OK + status: 403 + reason: Forbidden headers: fields: - [ Content-Length, "6" ] @@ -214,7 +215,7 @@ sessions: data: "second" proxy-response: - status: 200 + status: 403 headers: fields: - [ Content-Length, { value: "9", as: equal } ] @@ -255,6 +256,7 @@ sessions: # Block 3b verification: cache-bypass probe for SEND_RESPONSE. # Second response on repeated URL should still be replaced. + # Keep this as non-200 so null_transform does not engage. - client-request: method: "GET" version: "1.1" @@ -265,8 +267,8 @@ sessions: - [ uuid, 6 ] server-response: - status: 200 - reason: OK + status: 403 + reason: Forbidden headers: fields: - [ Content-Length, "6" ] @@ -276,7 +278,7 @@ sessions: data: "second" proxy-response: - status: 200 + status: 403 headers: fields: - [ Content-Length, { value: "9", as: equal } ] From 6b115ce545446fb85b501351d725c31fe1bef8e5 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 23 Apr 2026 13:04:54 -0700 Subject: [PATCH 3/5] Limit internal-body transform bypass to origin-response contexts. Guard the bypass path with server-response validity so non-origin regression flows (e.g. connect intercept tests) are not routed through origin replacement logic. Made-with: Cursor --- src/proxy/http/HttpSM.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index bfddfa6ac01..4599f6bebc4 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -1687,7 +1687,7 @@ HttpSM::handle_api_return() switch (t_state.next_action) { case HttpTransact::StateMachineAction_t::TRANSFORM_READ: { - if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { + if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set && t_state.hdr_info.server_response.valid()) { SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform for internal transfer"); t_state.api_info.cache_untransformed = true; if (tunnel.is_tunnel_active()) { @@ -8259,7 +8259,8 @@ HttpSM::set_next_state() case HttpTransact::StateMachineAction_t::SERVER_READ: { t_state.source = HttpTransact::Source_t::HTTP_ORIGIN_SERVER; - if (transform_info.vc && t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { + if (transform_info.vc && t_state.internal_msg_buffer && !t_state.api_server_request_body_set && + t_state.hdr_info.server_response.valid()) { SMDbg(dbg_ctl_http, "plugin set internal body, bypassing response transform"); t_state.api_info.cache_untransformed = true; if (transform_info.entry != nullptr) { From ce43606e3392f1b7297080890929917f81e4ada8 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 23 Apr 2026 16:10:13 -0700 Subject: [PATCH 4/5] Restrict internal response-body override to response-hook contexts. Prevent TSHttpConnect intercept regressions from taking the set-body internal-transfer path by guarding the SERVER_READ override on origin-response hook context and plugin tunnel state. Made-with: Cursor --- src/proxy/http/HttpSM.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index 4599f6bebc4..c75fd0148fa 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -1743,7 +1743,10 @@ HttpSM::handle_api_return() } setup_blind_tunnel(true, initial_data); - } else if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set) { + } else if (t_state.internal_msg_buffer && !t_state.api_server_request_body_set && t_state.hdr_info.server_response.valid() && + plugin_tunnel == nullptr && + (api_hooks.get(TS_HTTP_READ_RESPONSE_HDR_HOOK) != nullptr || + api_hooks.get(TS_HTTP_SEND_RESPONSE_HDR_HOOK) != nullptr)) { // A plugin replaced the origin response body via TSHttpTxnErrorBodySet(). // Serve the synthetic body before entering the response body tunnel. SMDbg(dbg_ctl_http, "plugin set internal body, using internal transfer instead of server tunnel"); From 112468f20ece40c470045bd607cc5ce0b1fe05c2 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 24 Apr 2026 13:29:38 -0700 Subject: [PATCH 5/5] Harden transform cleanup for set-body bypass path. Prevent null transform hook cleanup crashes when internal response-body replacement bypasses transform setup, fixing -11 failures in header_rewrite_set_body_origin and filter_body AuTests. Made-with: Cursor --- src/proxy/http/HttpSM.cc | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index c75fd0148fa..e07dc8981e4 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -7610,12 +7610,19 @@ HttpSM::setup_client_request_plugin_agents(HttpTunnelProducer *p, int num_header inline void HttpSM::transform_cleanup(TSHttpHookID hook, HttpTransformInfo *info) { + if (info->entry == nullptr) { + return; + } APIHook *t_hook = api_hooks.get(hook); if (t_hook && info->vc == nullptr) { do { - VConnection *t_vcon = t_hook->m_cont; - t_vcon->do_io_close(); - t_hook = t_hook->m_link.next; + APIHook *next = t_hook->m_link.next; + // Some transform hooks can already be detached by the time kill_this() runs. + // Guard against null continuations while still draining the remaining hooks. + if (auto *t_vcon = static_cast(t_hook->m_cont); t_vcon != nullptr) { + t_vcon->do_io_close(); + } + t_hook = next; } while (t_hook != nullptr); } } @@ -7696,7 +7703,11 @@ HttpSM::kill_this() // In that case, we need to manually close all the // transforms to prevent memory leaks (INKqa06147) if (hooks_set) { - transform_cleanup(TS_HTTP_RESPONSE_TRANSFORM_HOOK, &transform_info); + bool bypassed_response_transform = + t_state.api_info.cache_untransformed && t_state.internal_msg_buffer && !t_state.api_server_request_body_set; + if (!bypassed_response_transform) { + transform_cleanup(TS_HTTP_RESPONSE_TRANSFORM_HOOK, &transform_info); + } transform_cleanup(TS_HTTP_REQUEST_TRANSFORM_HOOK, &post_transform_info); plugin_agents_cleanup(); }