Experimental HTTP QUERY

Adopt safe body-bearing searches with explicit routes, GET fallbacks, and verified deployment boundaries.

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

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

  • Prefer307/308when the client must repeat QUERY and its body.
  • Use303to hand off to a GET resource.
  • Test301/302custom-method behavior in the actual client.
  • Use application-owned opaqueLocation/Content-Locationvalues; never encode sensitive query content into a URI.
  • ApplicationETag and Last-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.