You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bc...@apache.org on 2026/04/23 19:09:52 UTC

(trafficserver) branch set-body-origin-response-clean created (now 700f6bf1f0)

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

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


      at 700f6bf1f0 Enable robust set-body origin replacement across hooks and transforms.

This branch includes the following new commits:

     new 700f6bf1f0 Enable robust set-body origin replacement across hooks and transforms.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(trafficserver) 01/01: Enable robust set-body origin replacement across hooks and transforms.

Posted by bc...@apache.org.
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 <bc...@apache.org>
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}