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:

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:

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