header_rewrite: fix set-body origin replacement across hooks and transforms#13116
header_rewrite: fix set-body origin replacement across hooks and transforms#13116
Conversation
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
|
/copilot review |
There was a problem hiding this comment.
Pull request overview
This PR extends header_rewrite’s set-body operator to robustly replace origin response bodies (including across hooks and in the presence of response transforms), updates core HttpSM handling to honor the internal message buffer before tunneling, and adds/updates AuTests and documentation to cover the new behavior.
Changes:
- Update
HttpSMto divert origin responses tosetup_internal_transfer()when a plugin has setinternal_msg_buffer(including transform-related paths). - Allow
set-bodyto run atTS_HTTP_READ_RESPONSE_HDR_HOOK(in addition toTS_HTTP_SEND_RESPONSE_HDR_HOOK). - Add a replay-driven gold test matrix (read/send hooks; with/without transform) plus documentation updates.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/proxy/http/HttpSM.cc |
Adds internal-body override handling in SERVER_READ / TRANSFORM_READ and transform-bypass logic in set_next_state(). |
src/api/InkAPI.cc |
Ensures TSHttpTxnErrorBodySet() clears request-body override mode when reusing the shared buffer. |
plugins/header_rewrite/operators.cc |
Allows OperatorSetBody to run on TS_HTTP_READ_RESPONSE_HDR_HOOK. |
tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.test.py |
Adds a new replay-driven test wrapper for origin replacement scenarios. |
tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_origin.replay.yaml |
Adds replay matrix for read/send hooks and transform/no-transform scenarios. |
tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_read_resp.conf |
New rule file to apply set-body at READ_RESPONSE_HDR_HOOK. |
tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_origin_send_resp.conf |
New rule file to apply set-body at SEND_RESPONSE_HDR_HOOK. |
doc/admin-guide/plugins/header_rewrite.en.rst |
Documents origin replacement support and related caveats for set-body. |
tests/prepare_proxy_verifier.sh |
Updates expected Proxy Verifier tarball SHA1. |
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
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
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
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
Add focused comments in HttpSM transform-read handling to explain why tunnel teardown, transform VC cleanup, response header seeding, and server session release are required before internal transfer. Made-with: Cursor
Clarify transform-inactive test/docs wording, assert Content-Type in transform-active set-body cases, and document why transform cleanup remains guarded in the internal-body bypass path to avoid the known -11 crash. Made-with: Cursor
zwoop
left a comment
There was a problem hiding this comment.
I think this looks good, complex code, so love the tests. Other than all the comment slop, my only question really is around that hook check. That seems unnecessary, albeit I'm not 100% sure. Possibly they should be assertions, but I don't see how you'd get there, if you don't have those plugin hooks.
| setup_blind_tunnel(true, initial_data); | ||
| } 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 || |
There was a problem hiding this comment.
Is it even possible to get here, without one of these being true ? What's the intent here, that the plugin API wasn't called from somewhere else? But then we wouldn't have a server_response.valid() either.
I don't know for sure, but feels like either this check on the hook is just superfluous, or possibly should be a assert instead?
| 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. |
There was a problem hiding this comment.
Overall, and I'll start that here:
Claude has a big tendency to add comments that were part of its "thinking" phase. Explaining its reasoning for doing stuff. More often than not, I find those comments to be completely unnecessary, and often distracting, when the code itself is self explanatory. In most cases, the comments can be significantly reduced, and sometimes removed.
I'll refer to this as comment slop :).
| HttpTunnelProducer *p = setup_transfer_from_transform(); | ||
| perform_transform_cache_write_action(); | ||
| tunnel.tunnel_run(p); | ||
| // A plugin has installed an internal response body (TSHttpTxnErrorBodySet()). |
There was a problem hiding this comment.
Comment slop, code is self explanatory.
| 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 a tunnel was already set up for transform I/O, shut it down before we re-route. |
There was a problem hiding this comment.
Comment slop, code says exactly the same thing as the comment.
| t_state.hdr_info.client_response.create(HTTPType::RESPONSE); | ||
| t_state.hdr_info.client_response.copy(&t_state.hdr_info.transform_response); | ||
| } | ||
| // The server session is not needed for internal body transfer if it was never tunneled. |
| 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(). |
| HttpSM::transform_cleanup(TSHttpHookID hook, HttpTransformInfo *info) | ||
| { | ||
| // Internal-body bypass can skip transform tunnel setup, leaving no transform | ||
| // entry to clean up. In that case there is nothing safe/useful to close here. |
| 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. |
There was a problem hiding this comment.
The first comment seems useful, the second is slop.
| bool bypassed_response_transform = | ||
| t_state.api_info.cache_untransformed && t_state.internal_msg_buffer && !t_state.api_server_request_body_set; | ||
| // If we intentionally bypassed response transforms for internal-body | ||
| // transfer, transform_info may be partially detached; skip cleanup in this |
There was a problem hiding this comment.
Partial comment slop, can be reduced to one line.
| 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" |
There was a problem hiding this comment.
What happened here? We bumping proxy verifier version or something as part of this test suite? Feels like that shouldn't be the case, and if we need to, should be a separate PR ?
Summary
set-bodyrobust for origin responses by correctly handling internal body replacement before tunneling, including transform-bypass safety inHttpSMREAD_RESPONSE_HDR_HOOKas an allowedset-bodyhook while preservingSEND_RESPONSE_HDR_HOOK, and fix API state interaction inTSHttpTxnErrorBodySet()Production validation
Tested with live traffic on a single production edge/cache host using a build from this change set: staged
header_rewrite, thentraffic_server, with drain/restart and post-restart checks; exercisedset-bodyonREAD_RESPONSE_HDRfor origin 403 responses via config reload. Behavior matched expectations during rollout observation; the host was returned to rotation and informal bake/monitoring continues.Ready for review / merge from the author side.
Test plan
AUTEST_PORT_OFFSET=13000 ./autest.sh --ats-bin /tmp/ts-autest/bin --filter=header_rewrite_set_body_origin(eris)AUTEST_PORT_OFFSET=15000 ./autest.sh --ats-bin /tmp/ts-autest/bin --filter=header_rewrite_bundle(eris)Supersedes #12879.