Skip to content

Support the HTTP QUERY method (RFC 10008) #1441

Description

@pi0x

Note

This proposal was drafted with AI assistance

Support the HTTP QUERY method (RFC 10008)

RFC 10008 defines the HTTP QUERY method: a request that carries a body (like POST) but is safe, idempotent, and cacheable (like GET). It closes the long-standing "GET with a body" gap — clients send query criteria in the request body with a Content-Type, and the server treats the request as a read. The RFC also introduces the Accept-Query response header (a Structured Fields List) that advertises which query media types a resource accepts.

Now that this is a Proposed Standard, h3 should treat QUERY as a first-class method alongside the others.

Proposal

1. First-class QUERY method (core)

  • Add "QUERY" to the HTTPMethod union (src/types/h3.ts).
  • Add "QUERY" to the method loop in src/h3.ts so app.query(route, handler, opts) and app.on("QUERY", …) work.
  • defineRoute picks up the new method automatically via the HTTPMethod type.
  • rou3 matches on the method string, so routing needs no change — confirm with tests.

2. Body handling on a read method

  • Verify readBody / readValidatedBody don't short-circuit on non-POST/PUT/PATCH methods (they key off the request, so likely fine).
  • Ensure body-size limits (assertBodySize / bodyLimit) apply to QUERY, since the body is attacker-controlled.

3. Accept-Query — header utilities only (no core change)

h3 core does not introspect the route table to emit Allow/OPTIONS today (that logic doesn't exist in the request path — Allow is only ever built manually by utilities like assertMethod and serveStatic). So auto-deriving Accept-Query from registered routes would mean adding net-new core surface (route-table introspection + an OPTIONS/405 responder). We want to avoid that.

Instead, ship a symmetric header pair in src/utils/, mirroring getCookie/setCookie:

  • setAcceptQuery(event, mediaTypes) — serializes to the Accept-Query response header (Structured Fields List, e.g. application/sql;charset="UTF-8", application/jsonpath).
  • getAcceptQuery(event) — parses the header back into string[]. Reads the response header set wrote (the header is the single source of truth).

No route introspection, no pipeline hook. "Auto-derived" ergonomics remain possible in userland by calling setAcceptQuery from an OPTIONS handler; core stays untouched.

4. Content-Type validation — opt-in helper

  • The RFC mandates 400 (missing Content-Type), 415 (unsupported media type), and 422 (well-formed but unprocessable). Provide an opt-in helper (e.g. requireContentType(event, acceptedTypes)) that throws the appropriate HTTPError. Not enforced in core — keeps the core minimal.

5. Audits (parity with GET/POST)

  • CORS: QUERY is not CORS-safelisted, so browsers preflight it. h3's CORS is already method-agnostic (methods: "*"), so no logic change expected — confirm the preflight path and update docs/examples.
  • Caching: handleCacheHeaders should accept QUERY like GET for If-None-Match / If-Modified-Since / 304. Audit for GET/HEAD-only guards.
  • Proxy: confirm proxy / proxyRequest forward QUERY with its body (some proxy paths strip bodies for GET/HEAD — don't lump QUERY in).
  • Runtimes: verify each adapter + srvx pass a non-standard method token through (add a describeMatrix test hitting a QUERY route in both web and node modes).

Decisions

  • ✅ Add app.query() as a first-class method.
  • ✅ Content-Type validation is opt-in via a helper (not enforced in core).
  • Accept-Query is handled by a setAcceptQuery / getAcceptQuery utility pair (no core route-table introspection). get reads back what set wrote.

Suggested sequencing (small PRs)

  1. Core method support — type + route generation + matrix tests.
  2. setAcceptQuery / getAcceptQuery utilities.
  3. Opt-in Content-Type validation helper (400/415/422).
  4. Audits: handleCacheHeaders, proxy, bodyLimit parity + regression tests.

Progress checklist

PR 1 — Core method support (#1445 ✅ approved)

  • "QUERY" added to HTTPMethod union (src/types/h3.ts)
  • app.query() + app.on("QUERY", …) via method loop (src/h3.ts)
  • defineRoute picks up QUERY via HTTPMethod
  • readBody / readValidatedBody work on QUERY (not method-gated) — tested
  • assertBodySize / body-limit parity — tested
  • proxy / proxyRequest forward QUERY with body (QUERY added to PayloadMethods) — tested
  • Routing verified in web + node matrix (router/body/proxy tests)

PR 2 — Accept-Query header utilities

  • setAcceptQuery(event, mediaTypes) — serialize to Structured Fields List
  • getAcceptQuery(event) — parse header back to string[]

PR 3 — Opt-in Content-Type validation helper

  • requireContentType(event, acceptedTypes)400 missing / 415 unsupported / 422 unprocessable

PR 4 — Cross-cutting audits + docs

  • CORS preflight confirmed for QUERY (non-safelisted); document that explicit methods allowlists must include "QUERY"
  • handleCacheHeaders accepts QUERY like GET (If-None-Match / If-Modified-Since / 304) — audit GET/HEAD-only guards + regression test
  • Docs/examples for app.query() and the QUERY use case

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions