Skip to content

feat: get option for defineQueryHandler (cacheable GET equivalence)#1449

Merged
pi0 merged 2 commits into
feat/define-query-handlerfrom
feat/query-get
Jul 10, 2026
Merged

feat: get option for defineQueryHandler (cacheable GET equivalence)#1449
pi0 merged 2 commits into
feat/define-query-handlerfrom
feat/query-get

Conversation

@pi0x

@pi0x pi0x commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What

Stacked on #1448. Adds a get option to defineQueryHandler implementing RFC 10008 §2.3's cacheability guidance end-to-end: the same handler serves an equivalent, HTTP-cacheable GET, and successful QUERY responses advertise it via Content-Location — no server-side result store needed.

const searchBooks = defineQueryHandler({
  formats: ["application/sql", "application/jsonpath"],
  get: "q",
  handler: (event, { format, query }) => runQuery(format, query),
});
app.get("/books", searchBooks).query("/books", searchBooks);
// QUERY /books      -> 200 + Content-Location: /books?q=<query>&format=<format>
// GET  /books?q=... -> same result, ordinary HTTP caching applies
// HEAD /books?q=... -> same headers, empty body (via #1452's automatic GET matching)

Behavior

  • With get set, the handler context gains query on both paths (body text on QUERY, URL param on GET/HEAD); without it, behavior is unchanged (typed via overloads).
  • GET format comes from ?format= (customizable: get: { param, formatParam }); it may be omitted only when exactly one concrete (non-wildcard) format is accepted — silent defaulting across formats would change query semantics. All GET-path rejections are 400 (per RFC 9110, 415/422 apply to request content, which a GET has none of).
  • Content-Location preserves the request's existing search params (a QUERY to /books?lang=en advertises /books?lang=en&q=…), is built via URLSearchParams, is skipped when the URL would exceed 2048 chars (long queries being the reason QUERY exists), and is never set on the GET path.
  • HEAD needs no extra registration: h3 matches the GET route automatically (feat: automatically match GET routes for HEAD requests #1452), the handler runs with event.req.method === "HEAD", and the body is stripped downstream — so clients can revalidate/probe the advertised URL cheaply.
  • Method guard becomes 405 + Allow: GET, HEAD, QUERY; Accept-Query stays on every response including errors.

Design review

The plan was adversarially reviewed by an agent against the codebase and the RFC text before implementation. Notable outcomes:

  • Content-Location on 2xx confirmed as exactly RFC 10008 §2.3's mechanism (relative references valid per RFC 9110 §8.7).
  • A companion cache: {...} option (wiring handleCacheHeaders) was dropped: it duplicates a documented one-liner, and handleCacheHeaders currently force-prepends public to Cache-Control and ORs If-None-Match/If-Modified-Since (RFC 9110 §13.2.2 wants ETag to take precedence) — worth fixing upstream before building sugar on it. Can open a separate issue.

Changes

  • src/utils/query.ts: get option + overloads; src/utils/internal/query-get.ts: GET resolution + Content-Location helpers (keeps query.ts under the size guideline)
  • test/query.test.ts: 13 new matrix tests (round-trip identity, param preservation, encoding, wildcard/ambiguous/missing-param 400s, length cutoff, HEAD via automatic GET matching, 405 Allow)
  • examples/query.mjs: rewritten around get — drops the result Map, queryId hash, and /books/:id route (~30 lines shorter); curl comments include a HEAD probe
  • Docs: new "Offer a Cacheable GET Equivalent" section (links to the routing guide's "HEAD Requests" section from feat: automatically match GET routes for HEAD requests #1452); fixed a wording error (QUERY responses are cacheable per §2.7, just not URL-addressable)

Test plan

Disclaimer

🤖 This PR was written by Claude Code (design and implementation reviewed and directed by a human).

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9cb27371-0bec-4ea4-98cc-68b8d00d0f83

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/query-get

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

pi0 and others added 2 commits July 10, 2026 08:06
Relies on the HEAD->GET route fallback (#1452): with the `get` option,
plain .get()/.query() registration now also covers HEAD requests.

Co-Authored-By: Claude Fable 5 <[email protected]>
@pi0x
pi0x force-pushed the feat/define-query-handler branch from 9795969 to 3d8ad75 Compare July 10, 2026 08:06
@pi0x
pi0x force-pushed the feat/query-get branch from 03a8423 to f8af678 Compare July 10, 2026 08:06
@pi0
pi0 marked this pull request as ready for review July 10, 2026 08:07
@pi0
pi0 self-requested a review as a code owner July 10, 2026 08:07
@pi0
pi0 merged commit 5deb613 into feat/define-query-handler Jul 10, 2026
5 of 6 checks passed
@pi0
pi0 deleted the feat/query-get branch July 10, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants