Skip to content

hardik-goel/http-queryable

Repository files navigation

http-queryable

The new HTTP QUERY method is here (RFC 10008, IETF, June 2026) — but there's no easy, safe way to use it in Node. http-queryable gives you QUERY endpoints and correct body-aware caching in a few lines — for Express, Fastify, raw http, and an isomorphic browser/Node client.

npm CI license node types PRs welcome Contributor Covenant

http-queryable 30-second demo

Same-meaning body (re-spaced) → cache HIT and the handler doesn't re-run; a different body → the correct different result, never a stale hit.

🆕 New to the QUERY method? Understand it in 2 minutes

The problem QUERY solves. For decades, a "read with a big input" forced a bad choice:

Method Body? Safe & idempotent? Cacheable?
GET ❌ no (cram it into the URL, hit length limits) ✅ yes ✅ yes
POST ✅ yes ❌ no (can't safely retry; caches treat it as a write) ❌ no
QUERY yes yes yes

QUERY is "GET with a body" — a body like POST, but the semantics of GET: safe (no side effects), idempotent (retryable), cacheable. It's built for search / filter / graph-style reads that don't fit in a URL, without abusing POST.

QUERY /search HTTP/1.1
Content-Type: application/json

{ "q": "cats", "filters": { "color": "black" } }

Why caching it is the hard part. Shared HTTP caches key on method + URL. With QUERY, many different bodies hit the same URL:

QUERY /search  {"q":"cats"}   → cats results
QUERY /search  {"q":"dogs"}   → dogs results   ← same URL!

A method+URL cache would happily serve the cats response to the dogs request. That's a correctness/security bug — RFC 10008 §2.7 requires the request body to be part of the cache key. http-queryable does that correctly and conservatively: it only treats bodies as equal when it can prove they mean the same thing.

The golden rule: a false cache miss is harmless (you recompute); a false cache hit (two different bodies sharing a key) is a bug — so when in doubt, it caches less.

📖 Want the full rationale and the tricky edge cases? Read docs/why-query.md (5-min deep dive).


Quickstart — a QUERY endpoint in ~5 lines

npm install http-queryable express
import express from "express";
import { queryable, QueryCache } from "http-queryable/express";

const app = express();
app.use(queryable({ cache: new QueryCache() })); // QUERY + safe caching
app.query("/search", (req, res) => res.json(search(req.body)));
app.listen(3000);

Hit it with a body — QUERY is "GET with a body":

curl -X QUERY localhost:3000/search \
  -H 'content-type: application/json' \
  -d '{"q":"cats"}'

The differentiator

The only QUERY library that caches correctly — a different request body never gets another body's cached response.

Shared HTTP caches key on method + URL only. With QUERY, many different bodies hit the same URL, so a naive cache can serve the wrong result. RFC 10008 §2.7 requires the request body to be part of the cache key. http-queryable does exactly that — and does it conservatively, so it never produces a false hit.

curl -X QUERY /search -d '{"q":"cats"}'      # → X-Query-Cache: MISS  → cats result
curl -X QUERY /search -d '{ "q" : "cats" }'  # → X-Query-Cache: HIT   (same meaning, re-spaced)
curl -X QUERY /search -d '{"q":"dogs"}'      # → X-Query-Cache: MISS  → dogs result (NOT stale cats)

The second call is a hit because the body means the same thing (key order and whitespace don't matter). The third is a miss with the correct result because the body is genuinely different. Even the existing Fastify QUERY plugin punts on this and tells you to keep responses private — we solve it.

Why QUERY

  • GET-safe + POST-body + cacheable. Safe, idempotent reads that carry a real request body — no more cramming complex queries into the URL.
  • It's a standard. RFC 10008 (June 2026) defines the method, Accept-Query negotiation, and that the cache key includes the body.
  • Kills real pain. No more URL-length limits on big search/filter payloads, and no more abusing POST for reads (which breaks caching and idempotency).

Supported

Target Support
Express (4 & 5) app.query() + safe caching, CORS preflight, helpers
Fastify (≥5) ✓ plugin reusing the same caching core
raw node:http createQueryServer / createQueryListener
Browser + Node client ✓ isomorphic query(url, body, opts)

Requirements

Node >= 22. QUERY must be accepted by the runtime's HTTP parser; Node lists QUERY in http.METHODS starting at v22. http-queryable detects this at startup and fails with an actionable error on older Node.


The client (browser + Node)

import { query } from "http-queryable/client";

const { response } = await query("https://api.example.com/search", { q: "cats" });
const results = await response.json();
  • Sets Content-Type and serializes the body (JSON by default).
  • Optional Accept-Query discovery via OPTIONS ({ discover: true }).
  • Optional follow of Content-Location/Location to GET the canonical result ({ followResult: true }), per RFC 9110 §10.2.2.
  • Safe auto-retry with backoff — sound because QUERY is idempotent (RFC 10008 §2).

Fastify

import Fastify from "fastify";
import { fastifyQueryable, QueryCache } from "http-queryable/fastify";

const app = Fastify();
await app.register(fastifyQueryable, { cache: new QueryCache() });
app.route({
  method: "QUERY",
  url: "/search",
  handler: (req, reply) => reply.send(search(req.body)),
});

Raw http

import { createQueryServer, QueryCache } from "http-queryable/http";

createQueryServer(
  (req, res, ctx) => {
    res.setHeader("content-type", "application/json");
    res.end(JSON.stringify(search(ctx.body)));
  },
  { cache: new QueryCache() },
).listen(3000);

How the caching stays safe

The whole safety argument rests on one asymmetry:

  • A false miss (two equal bodies get different keys) is harmless — you recompute.
  • A false hit (two different bodies share a key) is a correctness/security bug — one client sees another's result. RFC 10008 Security Considerations flags exactly this.

So every normalization we apply is provably meaning-preserving, and when in doubt we normalize less:

Content-Type What we normalize What we never do
application/json, *+json insignificant whitespace, object key order, string-escape form merge numeric literals (10001e3); collapse big ints that lose precision; guess on duplicate keys (→ opaque)
application/x-www-form-urlencoded percent-encoding case, +/%20 reorder pairs
anything else (text, xml, octet-stream) nothing — treated as opaque cache by default (opt in with cacheOpaqueBodies, still exact-byte only)

Keys are length-prefixed, domain-separated, and SHA-256 hashed so no concatenation of URL/type/body can forge a collision. The engine also honors Cache-Control (no-store/private/max-age), cacheable status codes (RFC 9111), and ETag/If-None-Match conditional requests.

Accept-Query negotiation (RFC 10008 §3)

import { advertiseAcceptQuery, negotiateQueryType } from "http-queryable";

res.setHeader("Accept-Query", advertiseAcceptQuery(["application/json", "application/sql"]));
const chosen = negotiateQueryType(req.headers["accept-query"], ["application/json"]);

API surface

  • Core (http-queryable): deriveCacheKey, normalizeBody, canonicalizeJson, validateQueryRequest, advertiseAcceptQuery, parseAcceptQuery, negotiateQueryType, QueryCache, MemoryStore, CacheStore, isQueryMethodSupported, assertQueryMethodSupported.
  • /express: queryable(), ensureQueryMethod, sendAcceptQuery, contentLocation, rejectInvalidQuery.
  • /fastify: fastifyQueryable plugin (+ reply.acceptQuery, reply.contentLocation).
  • /http: createQueryServer, createQueryListener.
  • /client: query, discoverAcceptQuery.

Custom cache store

class RedisStore {
  /* get/set/delete/clear */
}
new QueryCache({ store: new RedisStore() });

http-queryable ships an in-memory LRU + the CacheStore interface — plug in Redis/Memcached/etc. We deliberately don't ship a cache backend; the value is the correct body-aware key.

Roadmap (future, not in v0.1)

  • Edge/CDN pass-through guidance and adapters.
  • GraphQL-over-QUERY integration.
  • Additional structured body types (XML/CBOR) with conservative canonicalization.

Contributing & community

Contributions are welcome — bug fixes, adapters, and conservative new body normalizers especially. Please read:

  • CONTRIBUTING.md — dev setup, evals-first testing, Conventional Commits, branch naming, and the DCO sign-off.
  • CODE_OF_CONDUCT.md — Contributor Covenant 2.1.
  • SECURITY.mdreport vulnerabilities privately, never in a public issue. Cache-key collisions are treated as security bugs.

Bugs and features go through the issue forms. See the changelog for release history.

License

MIT © http-queryable contributors

About

QUERY endpoints + correct body-aware caching for Node — the new HTTP QUERY method (RFC 10008) for Express, Fastify, raw http, and an isomorphic client.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors