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

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 78777f3373 ESI: --allowed-response-codes (#12459)
78777f3373 is described below

commit 78777f337363565097fe9d9345b43f19a94f09c2
Author: Brian Neradt <brian.ner...@gmail.com>
AuthorDate: Tue Aug 19 11:20:24 2025 -0500

    ESI: --allowed-response-codes (#12459)
    
    Adds --allowed-response-codes to the ESI plugin for response code
    filtering for which ESI transformations will take place. Some origins
    may add X-Esi headers to every response, even 5xx responses or the like.
    Setting those up for transformation can be wasteful.
---
 doc/admin-guide/plugins/esi.en.rst          |   3 +
 plugins/esi/esi.cc                          | 102 ++++++++++++++++++++--------
 tests/gold_tests/pluginTest/esi/esi.test.py |  30 +++++++-
 3 files changed, 105 insertions(+), 30 deletions(-)

diff --git a/doc/admin-guide/plugins/esi.en.rst 
b/doc/admin-guide/plugins/esi.en.rst
index 21855d4a40..0c0994ae51 100644
--- a/doc/admin-guide/plugins/esi.en.rst
+++ b/doc/admin-guide/plugins/esi.en.rst
@@ -92,6 +92,9 @@ Enabling ESI
   1024 * 1024.  Example values: 500, 5K, 2M.  If this option is omitted, the 
maximum document size defaults to 1M.
 - ``--max-inclusion-depth <max-depth>`` controls the maximum depth of 
recursive ESI inclusion allowed (between 0 and 9).
   Default is 3.
+- ``--allowed-response-codes <code1,code2,...>`` specifies a comma-separated 
list of HTTP response codes that should
+  be processed for ESI transformation.  Only responses with these status codes 
will be examined for ESI content. Default
+  is ``200,304``.
 
 3. ``HTTP_COOKIE`` variable support is turned off by default. It can be turned 
on with ``-f <handler_config>`` or
    ``-handler <handler_config>``. For example:
diff --git a/plugins/esi/esi.cc b/plugins/esi/esi.cc
index 229f68dac0..a93eae0ab2 100644
--- a/plugins/esi/esi.cc
+++ b/plugins/esi/esi.cc
@@ -33,9 +33,11 @@
 #include <limits>
 #include <arpa/inet.h>
 #include <getopt.h>
+#include <unordered_set>
 
 #include "ts/ts.h"
 #include "ts/remap.h"
+#include "swoc/TextView.h"
 
 #include "Utils.h"
 #include "gzip.h"
@@ -52,13 +54,16 @@ using std::string;
 using namespace EsiLib;
 using namespace Stats;
 
+using response_codes_t = std::unordered_set<int>;
+
 struct OptionInfo {
-  bool     packed_node_support{false};
-  bool     private_response{false};
-  bool     disable_gzip_output{false};
-  bool     first_byte_flush{false};
-  unsigned max_doc_size{1024 * 1024};
-  unsigned max_inclusion_depth{3};
+  bool             packed_node_support{false};
+  bool             private_response{false};
+  bool             disable_gzip_output{false};
+  bool             first_byte_flush{false};
+  unsigned         max_doc_size{1024 * 1024};
+  unsigned         max_inclusion_depth{3};
+  response_codes_t allowed_response_codes{200, 304};
 };
 
 static HandlerManager        *gHandlerManager = nullptr;
@@ -1314,17 +1319,20 @@ isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, 
const OptionInfo *pOptionI
     return false;
   }
 
-  // if origin returns status 304, check cached response instead
-  int response_status;
-  if (is_cache_txn == false) {
-    response_status = TSHttpHdrStatusGet(bufp, hdr_loc);
-    if (response_status == TS_HTTP_STATUS_NOT_MODIFIED) {
-      TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
-      header_obtained = TSHttpTxnCachedRespGet(txnp, &bufp, &hdr_loc);
-      if (header_obtained != TS_SUCCESS) {
-        TSError("[esi][%s] Couldn't get txn cache response header", 
__FUNCTION__);
-        return false;
-      }
+  int const response_status = TSHttpHdrStatusGet(bufp, hdr_loc);
+  Dbg(dbg_ctl_local, "Checking status: %d", response_status);
+  if (pOptionInfo->allowed_response_codes.find(response_status) == 
pOptionInfo->allowed_response_codes.end()) {
+    Dbg(dbg_ctl_local, "Not transforming response of status: %d (not in 
configured transform response codes)", response_status);
+    TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+    return false;
+  }
+  if (!is_cache_txn && response_status == TS_HTTP_STATUS_NOT_MODIFIED) {
+    // if origin returns status 304, check cached response instead
+    TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+    header_obtained = TSHttpTxnCachedRespGet(txnp, &bufp, &hdr_loc);
+    if (header_obtained != TS_SUCCESS) {
+      TSError("[esi][%s] Couldn't get txn cache response header", 
__FUNCTION__);
+      return false;
     }
   }
 
@@ -1614,18 +1622,19 @@ esiPluginInit(int argc, const char *argv[], OptionInfo 
*pOptionInfo)
   if (argc > 1) {
     int                        c;
     static const struct option longopts[] = {
-      {const_cast<char *>("packed-node-support"), no_argument,       nullptr, 
'n'},
-      {const_cast<char *>("private-response"),    no_argument,       nullptr, 
'p'},
-      {const_cast<char *>("disable-gzip-output"), no_argument,       nullptr, 
'z'},
-      {const_cast<char *>("first-byte-flush"),    no_argument,       nullptr, 
'b'},
-      {const_cast<char *>("handler-filename"),    required_argument, nullptr, 
'f'},
-      {const_cast<char *>("max-doc-size"),        required_argument, nullptr, 
'd'},
-      {const_cast<char *>("max-inclusion-depth"), required_argument, nullptr, 
'i'},
-      {nullptr,                                   0,                 nullptr, 
0  },
+      {const_cast<char *>("packed-node-support"),    no_argument,       
nullptr, 'n'},
+      {const_cast<char *>("private-response"),       no_argument,       
nullptr, 'p'},
+      {const_cast<char *>("disable-gzip-output"),    no_argument,       
nullptr, 'z'},
+      {const_cast<char *>("first-byte-flush"),       no_argument,       
nullptr, 'b'},
+      {const_cast<char *>("handler-filename"),       required_argument, 
nullptr, 'f'},
+      {const_cast<char *>("max-doc-size"),           required_argument, 
nullptr, 'd'},
+      {const_cast<char *>("max-inclusion-depth"),    required_argument, 
nullptr, 'i'},
+      {const_cast<char *>("allowed-response-codes"), required_argument, 
nullptr, 'r'},
+      {nullptr,                                      0,                 
nullptr, 0  },
     };
 
     int longindex = 0;
-    while ((c = getopt_long(argc, const_cast<char *const *>(argv), 
"npzbf:d:i:", longopts, &longindex)) != -1) {
+    while ((c = getopt_long(argc, const_cast<char *const *>(argv), 
"npzbf:d:i:r:", longopts, &longindex)) != -1) {
       switch (c) {
       case 'n':
         pOptionInfo->packed_node_support = true;
@@ -1679,6 +1688,34 @@ esiPluginInit(int argc, const char *argv[], OptionInfo 
*pOptionInfo)
         pOptionInfo->max_inclusion_depth = max;
         break;
       }
+      case 'r': {
+        // Parse comma-separated list of response codes using TextView.
+        pOptionInfo->allowed_response_codes.clear();
+        swoc::TextView codes_view(optarg);
+
+        while (codes_view) {
+          auto code_view = codes_view.take_prefix_at(',');
+          if (code_view.empty() && codes_view.empty()) {
+            break; // Handle trailing comma case
+          }
+
+          swoc::TextView parsed;
+          auto           code_value = swoc::svtou(code_view, &parsed);
+
+          // Check if the entire token was parsed and is a valid HTTP status 
code
+          if (parsed.size() == code_view.size() && code_value >= 100 && 
code_value < 600) {
+            
pOptionInfo->allowed_response_codes.insert(static_cast<int>(code_value));
+          } else if (!code_view.empty()) { // Ignore empty tokens (e.g., from 
trailing commas)
+            TSEmergency("[esi][%s] invalid response code format or value 
(%.*s) - must be between 100 and 599", __FUNCTION__,
+                        static_cast<int>(code_view.size()), code_view.data());
+          }
+        }
+
+        if (pOptionInfo->allowed_response_codes.empty()) {
+          TSEmergency("[esi][%s] no valid response codes specified", 
__FUNCTION__);
+        }
+        break;
+      }
       default:
         TSEmergency("[esi][%s] bad option", __FUNCTION__);
         return -1;
@@ -1686,12 +1723,21 @@ esiPluginInit(int argc, const char *argv[], OptionInfo 
*pOptionInfo)
     }
   }
 
+  // Format response codes for logging.
+  string response_codes_str = "";
+  for (auto const response_code : pOptionInfo->allowed_response_codes) {
+    if (!response_codes_str.empty()) {
+      response_codes_str += ",";
+    }
+    response_codes_str += std::to_string(response_code);
+  }
+
   Dbg(dbg_ctl_local,
       "[%s] Plugin started, "
       "packed-node-support: %d, private-response: %d, disable-gzip-output: %d, 
first-byte-flush: %d, max-doc-size %u, "
-      "max-inclusion-depth %u ",
+      "max-inclusion-depth %u, allowed-response-codes: [%s]",
       __FUNCTION__, pOptionInfo->packed_node_support, 
pOptionInfo->private_response, pOptionInfo->disable_gzip_output,
-      pOptionInfo->first_byte_flush, pOptionInfo->max_doc_size, 
pOptionInfo->max_inclusion_depth);
+      pOptionInfo->first_byte_flush, pOptionInfo->max_doc_size, 
pOptionInfo->max_inclusion_depth, response_codes_str.c_str());
 
   return 0;
 }
diff --git a/tests/gold_tests/pluginTest/esi/esi.test.py 
b/tests/gold_tests/pluginTest/esi/esi.test.py
index 0d83a8e5ee..9e647942b8 100644
--- a/tests/gold_tests/pluginTest/esi/esi.test.py
+++ b/tests/gold_tests/pluginTest/esi/esi.test.py
@@ -186,9 +186,9 @@ echo date('l jS \of F Y h:i:s A');
         client_process.Streams.stdout = "gold/esi_body.gold"
         if self._cc_behavior == CcBehaviorT.REMOVE_CC:
             client_process.Streams.stderr += Testers.ExcludesExpression(
-                'cache-control:', 'The Cache-Control field not be present in 
the response', reflags=re.IGNORECASE)
+                'cache-control:', 'The Cache-Control field should not be 
present in the response', reflags=re.IGNORECASE)
             client_process.Streams.stderr += Testers.ExcludesExpression(
-                'expires:', 'The Expires field not be present in the 
response', reflags=re.IGNORECASE)
+                'expires:', 'The Expires field should not be present in the 
response', reflags=re.IGNORECASE)
         if self._cc_behavior == CcBehaviorT.MAKE_PRIVATE:
             client_process.Streams.stderr += Testers.ContainsExpression(
                 'cache-control:.*max-age=0, private',
@@ -310,6 +310,22 @@ echo date('l jS \of F Y h:i:s A');
         tr.StillRunningAfter = self._server
         tr.StillRunningAfter = self._ts
 
+    def run_cases_expecting_no_transformation(self):
+        tr = Test.AddTestRun(f"Verify the ESI plugin does not transform 
responses: {self._plugin_config}")
+        client = tr.MakeCurlCommand(
+            f'http://127.0.0.1:{self._ts.Variables.port}/esi.php '
+            '-H"Host: www.example.com" -H"Accept: */*" --verbose',
+            ts=self._ts)
+        client.ReturnCode = 0
+
+        # Expect no transformation: the tag should be present without any 
transformation.
+        client.Streams.stdout += Testers.ContainsExpression(
+            'Hello, <esi:include src="http://www.example.com/date.php"/>',
+            'The response should not be transformed',
+            reflags=re.IGNORECASE)
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+
 
 #
 # Configure and run the test cases.
@@ -352,3 +368,13 @@ max_doc_2K_test = EsiTest(plugin_config='esi.so 
--max-doc-size 2K')
 max_doc_2K_test.run_cases_expecting_gzip()
 max_doc_20M_test = EsiTest(plugin_config='esi.so --max-doc-size 20M')
 max_doc_20M_test.run_cases_expecting_gzip()
+
+# The test doesn't use 304 redirect, so restricting the allowed response codes 
to 200
+# should not affect the test.
+allowed_response_codes_test = EsiTest(plugin_config='esi.so 
--allowed-response-codes 200')
+allowed_response_codes_test.run_cases_expecting_gzip()
+
+# Do not allow transforming the 200 OK response. Since the test uses a 200 OK 
response,
+# the plugin should not transform it.
+response_not_allowed_test = EsiTest(plugin_config='esi.so 
--allowed-response-codes 304')
+response_not_allowed_test.run_cases_expecting_no_transformation()

Reply via email to