Support JSON error response bodies - #13415
Conversation
c2e2428 to
857086d
Compare
857086d to
a791484
Compare
ATS cannot reliably emit JSON error responses through header rewrite: literal bodies cannot select a MIME type, HRW4U treats JSON braces as interpolation, fetched bodies lose their Content-Type, and conditional local files are unavailable. Body factory error pages are also hardcoded to HTML. This adds optional Content-Type arguments to set-body and set-body-from, preserves fetched response types, adds configuration-time local file bodies, and teaches HRW4U literal JSON braces and MIME-aware forms. It also adds body-factory Content-Type metadata and clears stale MIME state for empty fabricated bodies. Network set-body-from remains READ_RESPONSE-only because remap hooks cannot suspend for its asynchronous fetch. This covers remap-time errors with literal or file-backed bodies instead. This incorporates Bryan Call's body-factory work from apache#12947. Related: apache#10893 Fixes: apache#13393 Fixes: apache#11480 Co-authored-by: Bryan Call <bcall@apache.org>
a791484 to
e839112
Compare
bryancall
left a comment
There was a problem hiding this comment.
I read the whole diff across all 44 files. The feature is a good idea and the execution is mostly careful:
- The charset fix is carried through consistently, all 14 gold files updated plus
tests/gold_tests/ip_allow/gold/log.goldresponse sizes bumped 453 to 468, which is exactly the 15 bytes of; charset=utf-8. That is the kind of detail that usually gets missed. OperatorSetBodyFrom'sstruct sockaddr_in addr {}value-initializes what was previously an uninitialized stack struct, sosin_zerowas being passed toTSFetchUrluninitialized before this.- The hrw4u section restrictions are tightened correctly for
set-body-fromandset-body-from-file, matching the C++initialize_hooks(). - The new coverage is behavioral rather than decorative: both the default and custom Content-Type paths, the fetched Content-Type propagating, the explicit override winning, and the file-backed body working.
- I verified the documentation claims rather than taking them on trust. A missing
.body_factory_infodoes skip the whole set, sinceHttpBodySet::initreturns -1 onopen()failure, and the documented\n,\r,\tand escaped-quote support already exists in the tokenizer.
Requesting changes on one item, plus a rebase.
set-body-from-file swallows a load failure
plugins/header_rewrite/operators.cc, OperatorSetBodyFromFile::initialize and ::exec
initialize() does _body = swoc::file::load(path, ec); if (ec) { TSError(...); return; } and leaves _loaded = false without throwing. parse_config() still returns true, TSRemapNewInstance returns success, and the remap config reloads cleanly. From then on every matching request gets the default body-factory HTML page with Content-Type: text/html instead of the intended JSON, and exec()'s if (!_loaded) { return true; } emits nothing, so there is no runtime signal at all. The single startup TSError is easy to lose in a reload log.
I want to correct the framing I would otherwise have used here. Operator::initialize() being void is not the obstacle: the exception-based failure channel already exists and is already used for this exact case. OperatorIf::add_operator and RuleSet::add_operator wrap op->initialize(p) in try/catch and return false on std::exception, RulesConfig::parse_config propagates that, and TSRemapNewInstance turns it into TS_ERROR. More to the point, OperatorRunPlugin::initialize in this same file already throws std::runtime_error when the referenced plugin cannot be loaded, which is the identical class of failure.
So the fix is a one-line throw, not a lazy-load redesign, and it brings set-body-from-file in line with the convention the file already follows.
Also worth addressing
plugins/header_rewrite/operators.cc, OperatorSetBody::initialize The new _content_type.set_value(p.get_value(), this) gives meaning to trailing tokens that Parser::preprocess already collected into _val but that set-body previously discarded. An existing rule set-body Sorry, page not found tokenizes to _arg = "Sorry," and _val = "page not found". Before this PR only _arg was consumed, so the body was the already-wrong string Sorry,. After it, the client gets Content-Type: page not found. Nothing validates that the second argument resembles a MIME type and there is no warning that the operator saw more tokens than it expected. Emitting a bogus Content-Type to clients is worse than the previous truncated body, and it happens on reload with no diagnostic. Rejecting or warning when the second argument contains whitespace or lacks a / would catch both the legacy config and simple typos.
Smaller items
tools/hrw4u/src/tables.py:67set-bodyis registered withHTTP_SECTIONSeven though the C++ operator allows onlyTS_REMAP_PSEUDO_HOOKandTS_HTTP_SEND_RESPONSE_HDR_HOOK, soREAD_RESPONSE { set-body(...) }passes hrw4u validation and is rejected later at ATS config load. The same commit restricts the other two operators correctly, so the omission looks accidental. Pre-existinginbound.resp.bodyon line 46 has the same over-permissive sections, so this does not make things worse.plugins/header_rewrite/operators.cc:838-839The two newrequire_resources()calls inOperatorSetBody::initializeare dead.RSRC_SERVER_RESPONSE_HEADERSis only read underTS_HTTP_READ_RESPONSE_HDR_HOOK, andexec()reads neitherres.bufpnorres.resp_status. Harmless, but it was evidently copied fromOperatorSetBodyFromwhere the requirements are load-bearing, and a reader will assume a dependency that is not there.doc/admin-guide/plugins/header_rewrite.en.rst:~1206-1215Aset-bodyREMAP example is inserted into the middle of theset-body-fromsection, which orphans the sentence "Wherehttp://www.example.com/secondis the destination..." two blocks away from the example it refers to, and duplicates a block shown 30 lines earlier. The newset-body-from-filesection is also placed betweenset-bodyandset-body-from.src/proxy/http/HttpBodyFactory.cc:~1010-1014The new Content-Type directive inherits the.body_factory_infovalue parser's off-by-one:value_e = buffer + strlen(buffer) - 1assumes a trailing character to discard, so a file with no trailing newline yieldsapplication/jso. Pre-existing, since Content-Language and Content-Charset have always had it, but this PR adds a third directive that hits it and the new autest writes a trailing newline so the case is not covered. Worth a one-line fix while the file is open.- Test gaps on the degraded paths, which are the ones that fail quietly in production:
set-body-from-filewith an unreadable path,set-body "" "application/json"which clears the body but keeps the message buffer type sobuild_error_responsefabricates the default HTML page and then labels itapplication/json, and a body file larger thanproxy.config.body_factory.response_max_size, which becomes a zero-length body with no error. - Not introduced here and not a reason to hold the PR, but flagging so it is known rather than discovered from a sanitizer crash later:
SetBodyFromDatais rawnew/deletewhose lifetime is tied toTS_EVENT_HTTP_TXN_CLOSEon the same continuation that receives the fetch callbacks. A client abort while the internalTSFetchUrlis outstanding frees the state and destroys the continuation before the FetchSM delivers its result. The continuation was already destroyed at TXN_CLOSE before this PR; the change only adds owned heap state to the existing race.
The branch is conflicting with master and needs a rebase.
Problem
ATS cannot reliably emit JSON error responses through header rewrite: literal bodies cannot select a MIME type, HRW4U interprets JSON braces as interpolation, fetched bodies lose their Content-Type, and conditional local files are unavailable. Body factory error pages are also hardcoded to HTML.
Changes
set-bodyandset-body-from.set-body-from-file, loaded once at rule-configuration time.The asynchronous URL form remains
READ_RESPONSE-only because remap callbacks cannot suspend for an internal fetch. REMAP errors are handled by the literal or file-backed forms without origin or cache lookup.This includes the body-factory work from #12947 and preserves Bryan Call's original commits.
Related: #10893
Fixes: #13393
Fixes: #11480