The problem
RFC 10008 registers QUERY: a
method that is safe and idempotent like GET, but carries its query in the
request body rather than the URL. It exists for searches and filters too large or
too structured to fit in a query string.
Envoy’s handling of it depended on the protocol version. HTTP/2 and HTTP/3 keep no
method allowlist, so they forwarded it. HTTP/1 rejected it with a 400
(HPE_INVALID_METHOD), because the parser checks the method against a fixed list
of known methods that predates the RFC. The same request succeeded or failed
depending on how the client connected.
The only workaround was allow_custom_methods. It accepts any token as a method,
and the API still marks it as work in progress.
What I changed
- Recognised
QUERYin the HTTP/1 parser, so it’s routed on all three protocol versions. - Added it to the IANA method registry used by the default header validator, so
enabling
restrict_http_methodsno longer rejects a method that is now registered. - Guarded the change with
envoy.reloadable_features.http1_allow_query_method, since accepting a method that used to be rejected is a behaviour change operators can see. The guard is read once, when each connection’s parser is built, so per-request method validation does no extra runtime lookup. - Enforced the RFC’s
Content-Typerule: aQUERYwith noContent-Typegets a400. The check sits in the HTTP connection manager rather than a codec, which is where all three downstream protocols meet. It returns a plain400instead of a codec error that would drop the HTTP/1 connection. - Registered the
Content-LocationandAccept-Queryfield names, so future code doesn’t spell the strings out by hand.
Unit tests cover the parser, codec, header validator and connection manager. An
integration test routes a QUERY through every combination of downstream and
upstream protocol.
Judgement calls
QUERY is safe, but not by Envoy’s definition of safe. The RFC defines QUERY
as safe, yet I kept it out of isSafeRequest() on purpose. One reviewer questioned
the exclusion and another asked how it would interact with 0-RTT early data.
Tracing that through the code showed that callers of isSafeRequest() assume a safe
request has no body. That function decides whether a request may be sent as 0-RTT
early data, and whether a 425 triggers an automatic retry, which for a request
with a body means buffering the body. Marking QUERY safe would have changed both
behaviours without anyone deciding to. I left a TODO where the function is
implemented and a note at each call site, so a later change can’t alter either
behaviour by accident.
Only the missing Content-Type is enforced. The RFC also rejects a
Content-Type that doesn’t match the body. Checking that would mean buffering and
inspecting the content, and only the origin server knows which query formats it
supports. A proxy that guessed would do more harm than good, so the code comment
says this was deliberate rather than an oversight.
What review changed
Reviewers moved the change closer to the RFC: they asked for the Content-Type
rule to be enforced now rather than later, and for the two new header fields to be
registered. My first version put the Content-Type check behind a second runtime
guard of its own. The reviewers, and then the maintainer, agreed that one flag was
enough: the check can only be reached if HTTP/1 accepts QUERY, and that is already
guarded. So the second guard came out.
Outcome
Merged on 24 August 2026, closing #46404. It ships in Envoy’s next minor release after v1.39, with a release note under minor behaviour changes.