HTTP QUERY Method

Accept safe, cacheable requests that carry a query in the body.

The HTTP QUERY method (RFC 10008) is like GETsafe, idempotent, and cacheable — but carries a query in the request body with a Content-Type. It's the standard answer to "I need a GET, but my query is too large or too structured for the URL".

H3 supports QUERY as a first-class method via app.query(), plus two helper utilities.

Register a QUERY Handler

Read the request body just like you would for a POST:

import { readBody } from "h3";

app.query("/books", async (event) => {
  const query = await readBody(event, { type: "text" });
  return runSearch(query);
});

Because QUERY carries an attacker-controllable body, body-size limits apply just like POST.

Use appendAcceptQuery to tell clients which query formats a resource understands. It sets the Accept-Query response header (a Structured Fields List), and can be set on a plain GET too so clients can discover formats before sending a QUERY:

import { appendAcceptQuery } from "h3";

app.get("/books", (event) => {
  appendAcceptQuery(event, ["application/sql", "application/jsonpath"]);
  // Accept-Query: application/sql, application/jsonpath
  return "Send a QUERY request with a SQL or JSONPath body.";
});

Validate the Content-Type

Use requireContentType to enforce the RFC's error semantics. It returns the matched media type, or throws 400 (missing), 415 (unsupported), or 422 (malformed):

import { requireContentType, readBody } from "h3";

app.query("/books", async (event) => {
  const type = requireContentType(event, ["application/sql", "application/jsonpath"]);
  const query = await readBody(event, { type: "text" });
  return runQuery(type, query);
});

Offer a Cacheable GET Alternative

A QUERY response is not addressable by URL, so browsers and CDNs can't cache it. RFC 10008 suggests pointing clients at an equivalent, cacheable GET via the Content-Location header. Stash the result under a stable id and let a client repeat the query with an ordinary, HTTP-cacheable GET:

app.query("/books", async (event) => {
  const result = runQuery(type, query);
  const id = queryId(type, query); // stable hash of the query
  cache.set(id, result);
  event.res.headers.set("content-location", `/books/${id}`);
  return result;
});

Full Example

A self-contained, runnable demo — a /books resource that accepts SQL-ish and JSONPath queries, validates the Content-Type, and advertises a cacheable GET alternative. It also serves a small interactive page at /.

See the full examples/query.mjs source, or run it locally with node examples/query.mjs.
Unlike GET, QUERY is not CORS-safelisted, so browsers send a preflight. If you pass an explicit methods allowlist to handleCors, include "QUERY".