A tiny, dependency-free client for the HTTP QUERY method (RFC 10008) — the request that is safe and idempotent like GET, but carries a body like POST, and caches like neither before it could.
Built on native fetch. Works in Node 18+, Deno, Bun, Cloudflare Workers, and the browser.
import{query}from"@danmat/query-fetch";constres=awaitquery("https://api.example.com/search",{json: {filter: {status: "active"},sort: "-createdAt",limit: 50},});For years you had two bad options for a search endpoint:
GETwith a query string — safe, idempotent, cacheable… but your filter blows past URL length limits and leaks into logs.POSTwith a body — room for a rich query… but it's neither safe, idempotent, nor cacheable, so proxies and clients treat it as a state change.
QUERY is the missing third option: a body-carrying request that intermediaries may cache and clients may safely retry. This library handles the sharp edges the spec introduces.
npm install @danmat/query-fetchScripted fetch(url, { method: "QUERY", body }) already works in modern runtimes — but the semantics of RFC 10008 are on you. This library covers them:
- ✅ Enforces
Content-Type— the RFC requires servers to reject a QUERY whose body has no content type. We throw before the round-trip instead of letting you debug a400. - ✅ Transparent
POSTfallback — servers that don't understand QUERY yet respond405/501; we automatically retry asPOSTand advertise the original method viaX-HTTP-Method-Overrideso override-aware backends still route it correctly. - ✅
Acceptnegotiation — pass a media type (or list) to negotiate the response format the RFC'sAccept-Querydance is built around. - ✅ Safe automatic retry (opt-in) — QUERY is idempotent by definition, so retrying transient failures is safe here in a way it never is for
POST. Exponential backoff + jitter, honoursRetry-After. - ✅ Redirect-safe — the RFC's
303 See Otherindirect-result pattern is handled byfetch's own redirect following; nothing surprising here. - ✅ Zero dependencies, fully typed, tree-shakeable, dual ESM/CJS.
import{queryJson}from"@danmat/query-fetch";const{ data, response }=awaitqueryJson<{total: number}>("https://api.example.com/search",{json: {q: "http query method"}},);console.log(data.total,response.headers.get("age"));queryJson sets Accept: application/json, throws on a non-2xx status, and returns the parsed body alongside the raw Response.
awaitquery("https://api.example.com/search",{body: "SELECT * WHERE status = 'active'",contentType: "application/sql",accept: "application/json",});RFC 10008 defines QUERY as safe and idempotent — so unlike POST, retrying a
failed request can't cause a double-effect. Opt in with a count, or an object
for full control:
// Retry up to 3 times with exponential backoff + jitter.awaitquery(url,{ json,retry: 3});// Full control.awaitquery(url,{
json,retry: {retries: 5,minDelay: 200,// base backoff (ms)maxDelay: 10_000,factor: 2,jitter: true,respectRetryAfter: true,// honour Retry-After on 429/503retryOn: ({ response, error })=>Boolean(error)||(response?.status??0)>=500,onRetry: ({ attempt, delay })=>console.warn(`retry #${attempt} in ${delay}ms`),},});By default it retries network errors and 408/425/429/500/502/503/504, and
does not retry aborts. Retry is off unless you set it. (Retries reuse a
buffered body — a string, bytes, or json; a streaming body is sent once.)
awaitquery(url,{ json,fallbackToPost: false});import{fetchasundiciFetch}from"undici";awaitquery(url,{ json,fetch: undiciFetch});Performs a QUERY request. options extends RequestInit (so signal, credentials, redirect, etc. all work), minus method and with a richer body:
| Option | Type | Default | Description |
|---|---|---|---|
body | BodyInit | null | — | Raw query body. Pair with contentType. |
json | unknown | — | Value serialized to JSON; sets application/json. |
contentType | string | — | MIME type of body. Required when a body is present. |
accept | string | string[] | — | Sets the Accept header. |
fallbackToPost | boolean | true | Retry as POST on 405/501. |
methodOverrideHeader | string | false | "X-HTTP-Method-Override" | Header advertising the original method on fallback. |
retry | number | RetryOptions | off | Auto-retry transient failures (safe: QUERY is idempotent). |
fetch | typeof fetch | globalThis.fetch | Custom fetch implementation. |
query + JSON parsing + a non-2xx guard.
Thrown for construction-time problems (a body without a content type, no available fetch) and non-2xx responses in queryJson.
QUERY is a Proposed Standard (June 2026). Two things to know:
- CORS: QUERY is not a CORS-safelisted method, so a cross-origin QUERY triggers a preflight. Your server must handle
OPTIONSaccordingly. - Spec churn: browser-integration details (method normalization, caching) are still being ironed out in whatwg/fetch#1938. This library tracks runtime behavior as it ships.
@danmat/query-fetch— client for the QUERY method (you are here).@danmat/accept-query— parse/build/negotiate theAccept-Queryheader.@danmat/query-cache— body-aware response caching.@danmat/query-server— server-side request validation & negotiation.
MIT © Dan Matthew