This is an automated email from the ASF dual-hosted git repository.

bryancall pushed a commit to branch set-body-origin-response-clean
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 700f6bf1f0b7b2275c60e3762332c5156647232c
Author: Bryan Call <[email protected]>
AuthorDate: Thu Apr 23 10:05:42 2026 -0700

    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(-)

diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst 
b/doc/admin-guide/plugins/header_rewrite.en.rst
index 2d57f64f97..98cc6604cf 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 <text>
 
-Sets the body to ``<text>``. 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 ``<text>``. 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 20207bd24b..3e34efbe7e 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 2f62c45f75..423d26a043 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 c8a6b128dc..0022cc9ead 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 0000000000..d044814209
--- /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 0000000000..0eb53765b2
--- /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 0000000000..3d024c2c89
--- /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 0000000000..4d4321dee6
--- /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 3e853a30f1..8c570efc7f 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}

Reply via email to