A handler is what finally answers a request. Each site or route block has at most one. A block without one answers through its nested routes, or with 404 Not Found if none of them match.
| Handler | Answers with |
|---|---|
proxy <backend>... { } |
The response of an HTTP backend. See Reverse proxy |
files <root> { } |
Files from a directory. See Static files |
php <socket> [docroot] |
xTQ PHP, with a page cache, a waiting room and the browser check |
redirect <url> [status] |
A redirect |
respond [status] ["body"] |
A fixed response |
abort |
Nothing. The connection is closed |
proxy
proxy 127.0.0.1:3000 127.0.0.1:3001 {
health /health
}
Forwards the request to one of the listed backends and streams the response back. Backends are host:port, http://host:port or unix:/path. Options cover load balancing, health checks, timeouts, the Host header, extra request headers and the in-flight cap. Everything is on the Reverse proxy page.
files
files /var/www/example {
index index.html
not_found /404.html
}
Serves files from a directory, with index files, an optional fallback page, byte ranges, a memory cache and compression. The options are index, not_found and status. See Static files.
redirect
redirect <url> [301|302|307|308|permanent|temporary]
Answers with a redirect to url and an empty body. The status is:
| Written | Status |
|---|---|
nothing, or temporary |
302 Found |
permanent |
301 Moved Permanently |
| a number | That status. It must be a 3xx code, usually 301, 302, 307 or 308 |
307 and 308 tell the client to repeat the same method and body, which matters for forms and API calls. 301 and 302 let browsers switch to GET.
The URL may contain the placeholders {uri}, {path}, {query} and {host}, explained in Config file. It can be absolute (https://example.com/) or a path on the same site (/new/).
www to the apex, keeping the path and query:
site www.example.com {
redirect https://example.com{uri} 301
}
HTTP to HTTPS for every hostname on port 80:
site * {
bind 0.0.0.0:80
redirect https://{host}{uri} 301
}
A moved page:
route /old-pricing {
redirect /pricing 308
}
If the expanded URL can't be a header value, the answer is 400 Bad Request. Let's Encrypt challenges are answered before any redirect, so redirecting a hostname does not block its certificate.
respond
respond
respond <status>
respond "<body>"
respond <status> "<body>"
A fixed response. The status defaults to 200 and the body to empty. A non-empty body is sent with Content-Type: text/plain; charset=utf-8. With one argument, a number is taken as the status and anything else as the body, so respond 404 sends an empty 404 and respond "ok" sends 200 with ok.
route /health {
respond 200 "ok"
}
site * {
respond 404 "unknown site"
}
Use header in the same block to add headers or change the content type:
route /robots.txt {
respond 200 "User-agent: *\nDisallow: /\n"
}
route /status.json {
header Content-Type application/json
respond 200 "{\"ok\":true}"
}
abort
abort
Sends no response at all. Over HTTP/1.1 the connection is closed. Useful for hostnames or paths you never want to answer.
site * {
abort
}
An auth service answering 444 has the same effect.
php
php <socket> [docroot]
Connects to xTQ PHP on its Unix socket. The socket path may be written with or without a unix: prefix. The docroot is optional. Write - or leave it out for none. If you give one, it must exist when the config loads, or the config is refused.
site blog.example.com {
php /run/xtq/php.sock /var/www/blog
}
php takes no block. Its behaviour is tuned with environment variables, listed in Environment variables. php sites need Linux or macOS, since xTQ PHP listens on a Unix socket.
A request to a php handler goes through these steps.
Denied paths. If XTQW_DENY lists path prefixes, matching requests get 403 Forbidden. The path is decoded and lowercased before comparing.
Files from the docroot. For GET and HEAD, a regular file under the docroot is sent straight from disk, without PHP. Paths ending in / and files ending in .php, .phtml, .phar, .inc, .ini, .log, .sql or .sh never are. Symlinks that lead outside the docroot are refused. Hidden files are refused except under /.well-known/. These files get Cache-Control: public, max-age=2592000 for common asset types (CSS, JavaScript, images, fonts) and public, max-age=3600 for the rest. With XTQW_STATIC_404=1, a missing file with an asset extension gets a quick 404 with Cache-Control: public, max-age=300 instead of a PHP render.
The page cache. Anonymous GET and HEAD requests can be answered from memory. A request skips the cache when it has an Authorization header, a cookie whose name starts with wordpress_logged_in_, wordpress_sec_, wp-postpass_, comment_author_, woocommerce_, wp_woocommerce_session_, PHPSESSID, laravel_session or xtq_nocache, a path under /wp-admin, /wp-login.php, /xmlrpc.php, /wp-cron.php or /wp-json, or preview= in the query string.
A response is stored when its status is 200, 301 or 404, its body is at most 8 MB, it sets no cookie, and its Cache-Control has none of private, no-store, no-cache or max-age=0. The cache key is scheme, host, path and query.
A stored page is fresh for XTQW_CACHE_TTL seconds (default 10). After that it is served stale for up to XTQW_CACHE_STALE more seconds (default 86400) while one request renders a new copy in the background. When many requests ask for the same page that is not cached, only one goes to PHP and the others wait for its result. Pages from the cache carry an Age header and, unless the app set its own, Cache-Control: public, max-age=0, s-maxage=10, stale-while-revalidate=60. The cache shares XTQW_CACHE_MB of memory (default 1024) with the file cache. XTQW_CACHE=0 turns both off.
The X-Xtq-Cache response header says what happened: HIT, STALE, MISS, or BYPASS for a response that was not stored.
The browser check. With XTQW_CHALLENGE=1 (the default), xTQ Web asks xTQ PHP for its load every 150 ms. While the workers are at least XTQW_OVERLOAD_BUSY_PCT percent busy (default 90), or the queue is at least XTQW_OVERLOAD_QUEUE_PCT percent full (default 60), or PHP can't be reached, visitors without a pass get the browser check page instead of a render. Pages in the cache are still served to everyone. See Protection.
Fair share. Each client address may have at most XTQW_PER_IP_SLOTS requests (default 4) in PHP at once. Past that it gets 503 with Too many concurrent requests from your address, retry shortly, Retry-After: 1 and X-Xtq-Cache: FAIR-SHARE.
The waiting room. xTQ Web lets only as many requests into PHP as it can hold, its workers plus its queue, which it learns from xTQ PHP every 5 seconds. Others wait at the edge for up to XTQW_EDGE_WAIT_MS (default 3000). Visitors with a browser check pass wait in a separate lane for up to XTQW_PRIORITY_WAIT_MS (default 10000). If no slot frees up in time, the answer is 503 with Server busy, retry in a moment, Retry-After: 2 and X-Xtq-Cache: EDGE-FULL.
The render. The request goes to xTQ PHP with these headers added: X-Xtq-Client-Ip (the real client address), X-Xtq-Client-Port, X-Xtq-Https (on or off) and X-Xtq-Priority: 1 for visitors with a browser check pass. X-Forwarded-For, X-Forwarded-Proto, X-Real-Ip, True-Client-Ip and CF-Connecting-IP from the client are passed only when the client is a trusted proxy. Request bodies are read in full first, up to XTQW_MAX_BODY (default 64 MB, else 413 Payload Too Large) within XTQW_BODY_TIMEOUT seconds (default 60, else 408 Request Timeout).
Errors. No answer within XTQW_UPSTREAM_TIMEOUT seconds (default 120) gives 504 Gateway Timeout. A socket that can't be reached, or a broken reply, gives 503 with Service temporarily unavailable, retry shortly and Retry-After: 1.
Pages of 1 KB or more with a text content type are compressed with Brotli or gzip, whichever the client accepts, Brotli first. A page that did not come from the cache is compressed only when its status is 200.
