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 <[email protected]>
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()