QUERY method
The HTTP QUERY method evaluates a query expression against a document, a subtree, or a whole host, without a URL-length limit on the expression — it travels in the request body instead of a header. It is safe, idempotent, and cacheable, following the IETF draft draft-ietf-httpbis-safe-method-w-body.
QUERY accepts two request content types, which select entirely different behavior:
text/css-selector— a CSS selector, returning every matching element as amultipart/mixedresponse. Served by the WebDAV authoring server, and (for a single document) by the edge proxy.text/sessel— a Sessel expression, evaluated against a document. Served by the edge proxy.
Servers advertise the content types a resource accepts on OPTIONS responses via the Accept-Query header. Sending the wrong body type gets 415 Unsupported Media Type with Accept-Query naming what's accepted.
When to reach for it
Use the CSS-selector form when you need every match across a whole host or a directory, not just one document — something GET with Range: selector= can't do, since a selector range always targets a single resource. Use the Sessel form to compute a value from a document — a count, a filtered list, a single field — rather than fetching HTML and picking it apart client-side.
CSS-selector mode
On the WebDAV authoring server
QUERY on the authoring endpoint (dombase-webdav) selects its scope from the target URI:
| Target URI | Scope |
|---|---|
/ |
Every document on the host |
A path ending in / |
Every document under that prefix |
| Any other path | That single document |
QUERY /
Authorization: Basic SmFtZXMgRHVuY2FuOnNlY3JldA==
Content-Type: text/css-selector
h1
HTTP/1.1 201
etag: "85ec253dcbf0f9e47aa29c6bcf5ebb8e017f785e6286f49a736f8a6247f048cb"
content-type: text/html
<!DOCTYPE html>
<html><body><h1>Benji</h1></body></html>
Even a single-document query returns multipart/mixed — the response shape is the same at every scope, so a client doesn't need to special-case it.
Each part carries:
Content-Location— the document the match came from.Content-Range: selector <path>— a document-rooted CSS selector that addresses this specific match (not the request selector, which may match several elements) — round-trip it into a follow-upPUT/DELETEto touch just that element.- The matched fragment's
ETagandLast-Modified.
A query that matches nothing still returns 200 OK with an empty multipart body.
Conditional re-query. The top-level ETag is computed from the selector text plus every matched fragment's ETag, so it changes whenever the selector, the match set, or any matched fragment changes. Send it back as If-None-Match to get a cheap 304 Not Modified when nothing has:
QUERY /
Authorization: Basic SmFtZXMgRHVuY2FuOnNlY3JldA==
Content-Type: text/css-selector
If-None-Match: {{query_etag}}
h1
HTTP/1.1 200
etag: "135d9146abf272b94c84122fe86149389ca6c49196f65a6066518e62a6e3d6ca"
On the edge proxy
The public edge (dombase-http) also answers a text/css-selector QUERY, but only for a single, individually addressed document — not a host or subtree — and it queries the composed page (after templates, includes, and expressions have run), not the raw stored markup. It's a read, authorized the same way a GET of that path would be.
QUERY /query-composed-doc.html
Content-Type: text/css-selector
.calc
HTTP/1.1 201
<!DOCTYPE html>
<html xmlns:pagelove="https://pagelove.org/1.0"><body>
<main class="calc" pagelove:template="text/liquid">{{ 2 | plus: 3 }}</main>
</body></html>
Errors
| Condition | Status |
|---|---|
Missing or wrong Content-Type |
415 Unsupported Media Type (Accept-Query: text/css-selector) |
| Empty body, or a selector that fails to parse | 422 Unprocessable Content |
Accept header excludes multipart/mixed |
406 Not Acceptable |
| Target path doesn't exist | 404 Not Found |
Sessel mode
Send a Sessel expression as the body with Content-Type: text/sessel. self binds to the target document (QUERY /doc.html); querying a directory leaves self unbound, so an expression that references it (e.g. from self) fails with 416.
QUERY /team/james.html
Content-Type: text/sessel
${h1}.first()
HTTP/1.1 206
Content-Type: text/html
<main class="calc">5</main>
The response's Content-Type depends on what the expression evaluates to:
| Result | Content-Type |
|---|---|
| An element | text/html — the element's outer HTML |
| Anything else (a string, number, boolean, list, map, or null) | application/sessel+json |
QUERY /team/james.html
Content-Type: text/sessel
${li}.count()
HTTP/1.1 200
Content-Type: text/html
<h1>James</h1>
application/sessel+json is application/json plus one addition: an element embedded inside the result (say, from a list built with a schema search()) is wrapped as a tagged object rather than serialized as plain HTML text —
{
"$type": "element",
"$html": "<div id=\"widget\">...</div>",
"$source": "/team/benji.html"
}
— so a client can tell an embedded element apart from an ordinary string. $source is the document the element came from (omitted for an element built with new rather than selected from storage).
Paginating a list result
Send Range: entries=<start>-<end> alongside the query to paginate a list result the same way Range: entries= paginates any other list — see Reading and writing for the general mechanism. The response's Content-Range: entries <start>-<end>/<total> reports the slice against the full result length.
Errors
| Condition | Status |
|---|---|
Missing/wrong Content-Type, or an expression that fails to parse or evaluate, or an empty body |
400 Bad Request |
| Target document not found | 404 Not Found |
self unbound on a directory query, or entries= requested on a non-list result |
416 Range Not Satisfiable |
| Expression raised a runtime error | 500 Internal Server Error |
See also
- Sessel — the expression language QUERY's
text/sesselmode evaluates - Reading and writing —
GETand itsRangeunits, including the generalentries=pagination mechanism - OPTIONS method — discovering
Accept-Queryfor a resource - Accept-Ranges — the related header for
GET'sRangeunits