Framework-agnostic server helpers for the HTTP QUERY method (RFC 10008). Validate incoming QUERY requests, enforce the RFC's Content-Type rule, negotiate accepted query formats, and advertise them with Accept-Query.
Built on Web-standard Request/Response, so it runs anywhere they do — Hono, Deno, Bun, Cloudflare Workers, and Node (via a web adapter). Its only dependency is @danmat/accept-query.
import{checkQueryRequest,readQueryJson,withAcceptQuery}from"@danmat/query-server";constACCEPTED=["application/json","application/sql"];asyncfunctionhandler(request: Request): Promise<Response>{// Reject non-QUERY, missing/unsupported Content-Type — with correct status codes.constrejection=checkQueryRequest(request,{accept: ACCEPTED});if(rejection)returnwithAcceptQuery(rejection,ACCEPTED);constquery=awaitreadQueryJson<{filter: unknown}>(request);constresults=awaitrunQuery(query);returnwithAcceptQuery(Response.json(results),ACCEPTED);}RFC 10008 puts real obligations on the server: it MUST reject a QUERY whose Content-Type is missing, it should tell clients which query formats it accepts (via Accept-Query), and it needs to answer the method-override fallback that clients use when they're unsure the server speaks QUERY. This library packages those rules so your handler stays about your query logic.
npm install @danmat/query-serverWhether a request should be handled as a QUERY. Recognizes the QUERY method and, by default, POST + X-HTTP-Method-Override: QUERY (the fallback used by clients like @danmat/query-fetch). Disable with { allowMethodOverride: false }.
Throws a QueryRequestError (carrying the correct HTTP status and headers) when the request isn't a valid QUERY:
| Condition | Status | Extra |
|---|---|---|
| Not a QUERY request | 405 | Allow: QUERY |
Missing Content-Type | 400 | — |
Content-Type not in accept | 415 | Accept-Query: … |
Pass { accept: ["application/json", …] } to enable media-type negotiation (wildcards and parameters supported).
Non-throwing companion — returns a ready-to-send error Response, or null when the request is valid.
Reads the body as JSON, guarding the content type (415 for a non-JSON type, 400 for malformed JSON).
Builds an Accept-Query header value from the media types you accept (strings and/or structured ranges with q weights).
Returns a copy of response with the Accept-Query header set — handy on both success and 415 responses.
HTTP revalidation for QUERY results: attaches a strong ETag, and returns 304 Not Modified when the request's If-None-Match already matches (echoing Content-Location/Cache-Control/Vary). Only applied to 2xx responses. QUERY is safe and cacheable, so this is exactly the method conditional requests are meant for.
returnconditional(request,withContentLocation(withAcceptQuery(Response.json(results),ACCEPTED),request.url),);Returns a copy of response with a Content-Location header (the URL identifying the returned representation).
Computes a strong ETag (quoted SHA-256/base64url) for a body — equal bytes yield equal tags. Used by conditional, exposed for custom flows.
Error subclass with status: number, headers: Record<string,string>, and toResponse(): Response.
@danmat/query-fetch— client for the QUERY method.@danmat/accept-query— parse/build/negotiateAccept-Query.@danmat/query-cache— body-aware response caching.@danmat/query-server— server-side request validation & negotiation (you are here).
This suite's server behavior is exercised in rfc10008-interop — a neutral, co-maintained RFC 10008 interoperability matrix that runs a shared portable profile against the live example server and Ayder, recording capability-aware, side-by-side receipts.
MIT © Dan Matthew