Repository navigation
Tracking — QUERY readiness: working order and issue tree #37
Description
Single entry point for the project. Everything else hangs off #1–#7; this issue holds the order and the reason for it.
Context and evidence live in scope.md; decisions in docs/decisions/; measurements in experiments/ and matrix/.
⚠️ The order below changed on 2026-08-19. ADR 0002's lean reversed from C (opt-inQUERYABLE) to D (READABLEgainsQUERY) — see #10. D turnsQUERYon across the entire read surface at once, which made #2 a hard precondition rather than a preference, and briefly made #18 a gate. #18 is now closed — ADR 0003 accepted option C, core emits nothing method-specific, so that gate is discharged with no core work. One new issue, #38, is the last input the decision is waiting on.The prerequisite chain for proposing D — three items, one of them unstarted: #2/#14 (gap 3) → Trac#65905/#13136 → #38 (method-branching number) → #10 Accepted → #11 + #35 as one patch.
Suggested working order
1. #2 — gap 3, request body parsing ← start here
$accepts_body_data at class-wp-rest-request.php:377. One line.
Why first: it is the only gap that fails open. Measured 2026-08-16 on unmodified trunk — a QUERY carrying application/json already populates and schema-validates params correctly, but a form-encoded QUERY silently returns the full unfiltered collection with a 200. On a read endpoint that is a disclosure, not merely a wrong answer. Gaps 1 and 2 fail closed — a 404, a failed preflight — which is loud and harmless by comparison.
Every option under discussion in ADR 0002, including the QUERY → GET fallback, inherits that failure until this is fixed. Fixing it first takes the disclosure-shaped risk off the table before the contested decision is made.
Under D this is no longer just "first" — it is a precondition. D makes all 77 READABLE routes accept QUERY on upgrade, so shipping D before this would create the open failure on the whole read surface simultaneously. There is no ordering in which D lands first.
It is also the reason the lean moved. parse_body_params() already runs for QUERY (class-wp-rest-request.php:373-375); the parsed body sits in $this->params['POST'] and is merely left out of the lookup order. So this one line does not just fix QUERY bodies — it means every existing READABLE handler serves them correctly with no per-route work, which is the entire case for D.
- Decide ADR 0001 — QUERY body media type #13 ✅ — ADR 0001 decided 2026-08-19, option B: core parses
application/json+ form-encoded. Option D (pluggable per-route formats) declined — free to defer, because routes can already parse their own bodies viaget_body()with no core support - Add QUERY to $accepts_body_data + tests #14 — the patch + tests ← unblocked, and now the front of the queue. Nothing else in this issue is both decided and unstarted
- Write up the body-parsing rationale for the ticket #15 — the write-up for Trac
Two things stay out of #14, both recorded in ADR 0001: a 415 for unrecognized Content-Type (pre-existing, method-general, a new core opinion — parse_body_params() returns early at class-wp-rest-request.php:762-766, so application/jsonpath still falls through to a 200), and Accept-Query (#20). Keep the diff to one array entry.
2. #1 — gap 2, method constants
The most contested call. Leaning D as of 2026-08-19, not yet Accepted.
- Measure ADR 0002 blast radius across core's test suite #9 — blast radius ✅ measured — but the numbers do not discriminate between options; read the caveats before quoting them
- Measure the method-branching risk before proposing option D #38 — measure the method-branching risk ← the last input Decide ADR 0002 — ALLMETHODS vs QUERYABLE #10 is waiting on. Plugin code doing
'GET' === $request->get_method()silently stops matching under D. The only surviving objection that is still an assertion rather than a number - Decide ADR 0002 — ALLMETHODS vs QUERYABLE #10 — decide ADR 0002. Both of the original arguments against D have been falsified, and neither was measured before it was asserted. Cache safety is now discharged too (Decide ADR 0003 — cache-safety default #18, option C). Measure the method-branching risk before proposing option D #38 is the only remaining input
- Add the method constant + BC tests #11 — the patch + BC tests. Under D this gets smaller: one constant, no new constant, no route registration touched. But it must also fix
rest-site-health-controller.php:120-123, which usesREADABLEas an array key — #13136 does not cover that - Post the constant rationale + failure counts to Trac#65616 #12 — post the rationale + counts to Trac#65616
Propose D and put "on by default vs. opt-in" on the table as the explicit question. That is the real decision and it belongs to the core team, not to this project. D and E produce the identical dispatch outcome; the only difference is who decides.
3. #3 — gap 1, CORS preflight
- Prove and fix the rest_post_dispatch header clobber in rest_send_cors_headers() #16 — prove and fix the
rest_send_cors_headers()clobber. Patched upstream 2026-08-18 as wordpress-develop#13151, awaiting review. Independent ofQUERYentirely — see the parallel track below - File the CORS clobber as a standalone Trac ticket #17 ✅ — superseded. Went to the reopened Trac#46992 rather than a new ticket
- Add QUERY to the hardcoded CORS methods list (per #38546 precedent) #35 — add
QUERYto the methods list, per the Trac#38546 precedent. The target moved: #13151 replaced the hardcoded string with an array behind arest_allowed_cors_methodsfilter, so this is now one array element and rebases on Prove and fix the rest_post_dispatch header clobber in rest_send_cors_headers() #16. Under D it should probably ship as part of Add the method constant + BC tests #11 — a one-line constant plus a one-element array, with a test asserting the two agree, which is otherwise hard to enforce across separate patches
4. #4 — cache safety & Accept-Query
✅ Resolved 2026-08-19, and it cost no core work. This was put on the critical path that morning — under D,
QUERYreaches every read route, so caching stopped being a per-route question. The survey answered it in the other direction: no shipping edge cache stores aQUERYresponse at all. Varnishsynth(501)s it before the origin sees it, nginx's cache-method bitmask holds onlyGET/HEAD/POST,mod_cache/Squid/Fastly/Cloudflare allowlist, and RFC 9111 §3 already forbids storing a response whose method the cache has not understood. ADR 0003 accepted option C — emit nothing.This was the project's fifth self-correction, and the first one found by reading default config files rather than by measuring. The ADR had reasoned that because the failure was untestable, the worst case should be assumed. Documentary evidence sits between measurement and assumption, and the ADR skipped past it.
What is left here is Accept-Query, plus the follow-through from the survey.
- Decide ADR 0003 — cache-safety default #18 ✅ — ADR 0003 decided, option C. Closed
- Emit Accept-Query + the cache-safety default #20 — emit
Accept-Query. Scope halved — the cache-safety-default half is gone. Decide ADR 0001 — QUERY body media type #13 discharged the dependency but handed this a sub-decision rather than a value: core parses JSON and form-encoded, but advertising form-encoded is an endorsement rather than a bug fix, and probably should not happen.Accept-Queryis a compatibility surface from the moment it is emitted - Put the cache-poisoning rationale in the ticket explicitly #21 — inverted: publish the survey on Trac showing why no default is needed. Still not paperwork — a reviewer will ask about RFC 10008 §2.7, and silence reads as not considered
- Report the page-cache method denylists upstream (WP Super Cache, W3 Total Cache) #39 — the page-cache method denylists (WP Super Cache, W3 Total Cache). Both default-allow every future method; WPSC is saved only by an unrelated
REST_REQUESTexclusion. Reports written and parked,docs/submissions/—deferred, with a trigger rather than a date: filed when the feature plugin publishes (Publish the plugin as the "works on stock core" artifact #23) orQUERYdispatch ships in a core release (Add the method constant + BC tests #11). Nothing sendsQUERYtoday, so nothing is exposed by waiting, and a maintainer is right to deprioritize an unreachable hazard. Both trigger issues carry the hook in their checklists - Decide ADR 0004 — Location indirection #19 — decide ADR 0004 (
Locationindirection) —deferred. One argument for settling it early has gone: it would have made ADR 0003 less consequential, and ADR 0003 is now as inconsequential as it gets
The one live vector is W3TC Pro with REST caching enabled, and it is confusion, not disclosure — every parameter expressible in a QUERY body is expressible in a GET query string. Note also that a Cache-Control header, the obvious mitigation, would not have reached it: the only layer at risk decides in PHP on the write path and honors DONOTCACHEPAGE, which core references zero times.
5. #5 — feature plugin
The strongest single artifact for Trac: it demonstrates the end-to-end path on stock core with no patch at all.
- Run the feature plugin against a real WordPress install #22 — run it against a real install.
plugin/wp-http-query.phphas never been executed - Publish the plugin as the "works on stock core" artifact #23 — publish it
💡 #22 is cheap and out of order on purpose. It has no dependency on anything above and it is the highest evidence-per-hour item in the project. Worth doing early even though the plugin sits at position 5.
Under D it is worth more than that. The plugin's rest_request_parameter_order filter is the userland equivalent of the #14 one-liner, so running it demonstrates D's central claim on stock core with no patch: an existing READABLE handler, written years ago for GET, serving a form-encoded QUERY body correctly with zero per-route work. See the comment on #5 for what #22 should assert.
Parallel track — no dependency on any of the above
These land on their own merits whether or not the QUERY proposal ever ships. That is the point of keeping them separate: if the main effort stalls in review, these do not.
- Prove and fix the rest_post_dispatch header clobber in rest_send_cors_headers() #16 — the CORS header clobber. A real defect in core, framed without reference to
QUERY. Patch up, in review — #13151 - PHP client: WP_Http / Requests support for QUERY #6 — PHP client,
WP_Http/ Requests. Review, not authorship — Requests#1075 is in flight upstream (Review Requests#1075 against WordPress's needs #24, Help unblock test-server#13 #25, Update WP_Http's documented method contract after the upstream release #26, Comment findings on Requests#1074 / #1075 #27) - JS clients: api-fetch and browser fetch() #7 — JS clients:
api-fetch(Test @wordpress/api-fetch against source (Q7) #28) and browserfetch()(Test browser fetch() across Chrome / Firefox / Safari #29), published via Publish Axis C client results #30 - Document the hardening-allowlist 403 failure mode #32 — document the hardening-allowlist 403, and now the Varnish 501 as well. The first walls a real site owner hits, and no core patch fixes either. Varnish is the more serious of the two: it is the default page cache on much of managed hosting, only fixable in VCL, and a
501reads as "WordPress doesn't support this" - Axis A leftovers — HTTP/2, HTTP/3, TLS, OpenLiteSpeed #33 — matrix Axis A leftovers (HTTP/2, HTTP/3, TLS, OpenLiteSpeed) —
deferred - First adopter — /wp/v2/search QUERY handler #34 —
/wp/v2/searchas first adopter —deferred
Done
- Read Trac#65616 and record its contents #8 ✅ — Trac#65616 read in full. New, unowned, zero comments,
2nd-opinion. TheALLMETHODSrationale is not on Trac and probably was never written down, so ADR 0002 proceeds on the merits. Also cleared the browser dependency for Post the constant rationale + failure counts to Trac#65616 #12 and File the CORS clobber as a standalone Trac ticket #17 - Measure ADR 0002 blast radius across core's test suite #9 ✅ — ADR 0002 blast radius measured. Read the caveats: core uses
ALLMETHODSexactly once, on a route that self-defends, so the suite cannot measure option A at all — its0is not a safety result - Decide ADR 0003 — cache-safety default #18 ✅ — ADR 0003 decided, option C, core emits nothing. The threat model was falsified by a documentary survey of six edge caches and four page caches —
experiments/cache-survey/. Discharged a prerequisite instead of adding one. Spun off Report the page-cache method denylists upstream (WP Super Cache, W3 Total Cache) #39 (upstream plugin reports, written and parked untilQUERYis reachable) and a Varnish row on Document the hardening-allowlist 403 failure mode #32 - Decide ADR 0001 — QUERY body media type #13 ✅ — ADR 0001 decided, option B: core parses
application/json+ form-encoded. Option D (pluggable per-route formats) declined as revisitable — deferring it is free, because routes can already parse their own bodies viaget_body()with no core support at all - Re-file WebKit standards position #692 with the correct template #31 ✅ — WebKit standards position re-filed as #709
- File the CORS clobber as a standalone Trac ticket #17 ✅ — the clobber went to the reopened Trac#46992, not a standalone ticket. File the CORS clobber as a standalone Trac ticket #17 itself named that as the better outcome. Adam Silverstein, who closed #46992 as
invalidin 2019, has since agreed option C in person — the conditional and arest_allowed_cors_methodsfilter - File the array-form methods normalization bug as a standalone Trac ticket #36 ✅ — array-form
methodsnever comma-split, filed as Trac#65905, patched as wordpress-develop#13136. The severity framing in File the array-form methods normalization bug as a standalone Trac ticket #36 was wrong and is corrected there: 3 plugins / 2,000 installs hit it, not "plugins hit it now" at scale. Ticket argues the shape of the failure, not volume. Unlike File the CORS clobber as a standalone Trac ticket #17 it does disclose theQUERYmotivation — see Decide ADR 0002 — ALLMETHODS vs QUERYABLE #10 for why that overlap is the strongest argument available - Test matrix Axis A —
QUERY+ body reach PHP intact on nginx, Apache (both SAPIs) and Caddy, up to 64 KiB. Axis B descoped
In review upstream
Two core defects, both found by this project, both landable whether or not QUERY ships. Neither has a human review yet; CI is green on both.
| PR | Trac | Issue | Tests |
|---|---|---|---|
| #13136 — split comma-separated methods given in array form | #65905 | #36 | +4; 3 fail on trunk |
#13151 — stop clobbering Access-Control-Allow-Methods, add rest_allowed_cors_methods |
#46992 | #16 | 10 / 46; 6 fail on trunk |
Outstanding on both, blocked:human:
- Trac field housekeeping —
has-patchand a comment linking the PR - The AI-use disclosure core's PR template requires (AI Guidelines). Neither PR body carries one. Flagged in File the CORS clobber as a standalone Trac ticket #17 before either was opened and missed on both
Two standing rules
Every claim gets verified before it is published. The project's entire pitch is arriving with evidence rather than assertions, and this repo has now had to correct itself five times — the ADR 0002 tripwire premise, the argument against option E, the framing of Trac#46992, the silent-body-discard argument against option D, and ADR 0003's cache-poisoning threat model. Each correction was found by measuring or reading source we had only reasoned about, and every one of them favoured the status quo before it was checked. Assume the next one exists, and weight the remaining unmeasured claim (#38) accordingly.
The fifth one added a rule of its own: "we cannot measure it" is not a licence to assume the worst. ADR 0003 said so explicitly and then did it. Reading six caches' default configs took an afternoon and reversed the conclusion.
State the weaknesses in our own position first. The BC objection to option A is weaker than we assumed; option D's failure count is inflated by an unrelated bug; the #46992 clobber can be worked around by a plugin hooking at priority 11+; and D's method-branching risk is still unmeasured. Saying so first is worth more than hoping nobody checks.
Activity
docs: propagate the ADR 0003 decision to README, scope, and #37