HTTPQUERYis a safe, idempotent, body-bearing method for read-only queries
whose structured input would make an impractical URI. Chirp offers
experimental early-adopter support on explicit ASGI routes. Stable
promotion is not approved yet.
Use GET for ordinary, bookmarkable searches and every native HTML form. Choose QUERY only when the input is genuinely too large or structured for a useful URI, keep the handler free of requested mutations, and retain a GET fallback or equivalent GET resource.
Declare the route
from chirp import App, Page, Request
app = App()
@app.route(
"/search",
methods=["QUERY"],
query_media_types=("application/x-www-form-urlencoded",),
)
async def search(request: Request) -> Page:
form = await request.form()
results = await find_results(form)
return Page("search.html", "results", results=results)
query_media_typesis mandatory on QUERY routes and invalid elsewhere. Chirp
validates the media ranges at freeze time. Support is explicit-route-only:
there is no filesystemquery() convention, AppConfigswitch, or
QUERY-specific return type.
Tests can use the QUERY convenience method:
response = await client.query(
"/search",
data={"category": "books", "year": "2026"},
headers={"Accept": "text/html"},
)
query() accepts exactly one of raw body=, form-encoded data=, or JSON
json=. It supplies the standard content type for form and JSON bodies. Raw
bytes require an explicit media type:
response = await client.query(
"/search",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body=b"category=books&year=2026",
)
The generic client.request("QUERY", ...)surface remains available for full
wire-shape control. Both helpers traverse the same in-process ASGI path.
Failure and discovery contract
| Input or request | Result |
|---|---|
Missing/malformedContent-Type |
400 |
| Undeclared request media type | 415 plus Accept-Query |
| Body over the effective limit | 413 |
Response cannot satisfyAccept |
406 |
| Method mismatch | 405 with Allow and Accept-Query |
| Generated discovery | bodyless204 OPTIONSwith both headers |
An explicitOPTIONSroute wins. Parsers/handlers distinguish malformed query
content (400) from valid syntax that cannot be processed (422).
QUERY uses Chirp's normal return-type architecture and one-template/named-block
contract. A direct request can receive the fullPage; an htmx request can
receive the named results block from the same template. OOB,Stream,
Suspense, validation, redirects, and fail-loud missing-block behavior do not
gain a QUERY-specific side channel.
GET fallback and client boundary
Native forms cannot submit QUERY. Use<form method="get">for the
unenhanced path. Programmatic Fetch andhtmx.ajax("QUERY", ...)are covered
by browser tests, but Chirp does not publish a declarativehx-querysyntax or
stable adapter. Do not invent one in application docs.
The canonical complex QUERY search is executable proof: its JavaScript-disabled form submits only a compact, bookmarkable GET subset, while htmx sends the larger faceted input in a QUERY body and swaps the same template's named results block. There is no JSON response path, duplicate partial, or JavaScript build pipeline.
Cross-origin Fetch triggers a CORS preflight. Add QUERY to
CORSConfig.allow_methods; routes restricted to non-safelisted media ranges
also need the declaredContent-Type in allow_headers.
Catch literal wiring mistakes at startup
app.check() recognizes literal fetch("/path", {method: "QUERY", ...})and
htmx.ajax("QUERY", "/path", ...)calls. It errors when the URL is missing,
the route does not allow QUERY, the literalContent-Typeis unsupported, or
the call declares no headers. Dynamic URLs and header objects remain unknown;
the checker does not guess.
IfCORSMiddlewareallows origins for an app with QUERY routes, its
allow_methodsmust include QUERY. Routes that accept only non-CORS-safelisted
media ranges also needContent-Type or * in allow_headers; a route that can
accept form-urlencoded, multipart-form, or plain-text content does not. Run
chirp routes, autodoc, or the debug route explorer to inspect normalized
query_media_types. Inspection, static freeze, speculation rules, and contract
discovery never execute QUERY handlers.
Redirects, validators, and result identity
- Prefer
307/308when the client must repeat QUERY and its body. - Use
303to hand off to a GET resource. - Test
301/302custom-method behavior in the actual client. - Use application-owned opaque
Location/Content-Locationvalues; never encode sensitive query content into a URI. - Application
ETagandLast-Modifiedvalues participate in conditional evaluation and can produce304.
Attach source-specific validators to the normalResponse; Chirp evaluates
them after all middleware has finalized the representation:
return (
Response(rendered_results)
.with_header("ETag", f'W/"search-source-{source_revision}"')
.with_header("Last-Modified", source_modified_http_date)
)
Nonce-protected HTML is the safety exception. Chirp keeps its validators but
returns a fresh200and bypasses shared response caching, preventing cached
HTML containing nonce A from being reused under a new CSP containing nonce B.
Stable JSON, Markdown, and nonce-free HTML still use normal304semantics.
Cache only by explicit opt-in
Configuration-managed caching remains GET-only. A controlled experiment can manually supply Chirp's provisional body-aware key:
from chirp.cache.backends.memory import MemoryCacheBackend
from chirp.cache.key import query_cache_key
from chirp.cache.middleware import CacheMiddleware
app.add_middleware(
CacheMiddleware(
MemoryCacheBackend(),
ttl=60,
query_key_func=query_cache_key,
)
)
Private/authenticated requests, Set-Cookie, streaming/SSE, non-200 responses,
nonce-bearing HTML, and key/backend failures bypass. Use short TTLs,
application-specific vary headers, explicit invalidation, and a shared backend
when evaluating this in a multi-worker deployment.
Deployment boundary
- Verify every proxy/CDN preserves the method and body. Never rewrite QUERY to POST.
- Align Pounce and Chirp body limits; the lower limit wins.
- Keep body bytes out of logs, metrics, traces, and error capture unless a redaction policy explicitly permits them.
- Treat retry and HTTP/3 0-RTT as possible replay.
- Keep a direct-origin or GET fallback.
Chirp's matrix covers Pounce HTTP/1.1, HTTP/2, and HTTP/3, Uvicorn, Nginx, Chromium Fetch/CORS, redirects, retry, body limits, access logs, metrics, and traces. It certifies no CDN. See HTTP QUERY Interoperability for operator details.
Compatibility and release gate
| Capability | Status |
|---|---|
| Request/response protocol, typed rendering, cache opt-in, and tested transport matrix | Implemented |
| Filesystem/test-client ergonomics (#527) | TestClient.query()implemented; filesystem convention deferred |
| Declarative htmx plus no-JavaScript GET proof (#528) | Open |
| Literal-client diagnostics and non-execution proof (#533) | Implemented |
| Canonical complex-search example (#534) | Implemented and browser-tested |
| Stable/first-class promotion | Not approved |
The allowed claim is experimental early-adopter HTTP QUERY support. Do not claim native form support, universal intermediary compatibility, production-ready QUERY, or stable QUERY.
For implementation traceability and the final promotion checklist, see the canonical adoption guide and RFC 009.