A route picks part of a site by path and says how to treat it. This page covers every directive that can go in a route. How routes are matched and nested is in Config file.
| Directive | Where | What it does |
|---|---|---|
route <pattern> { } |
site, route | A nested route |
strip |
route | Removes the matched prefix from the path |
rewrite </path[?query]> |
site, route | Replaces the path, and the query if one is given |
header <Name> <value> |
site, route | Sets a response header |
header -<Name> |
site, route | Removes a response header |
ip <entries>... |
route | The route matches only for these client addresses |
not ip <entries>... |
route | The route matches only for other client addresses |
auth <backend> </path> |
site, route | Asks another service before the request is handled |
compress off |
site, route | Leaves proxied responses uncompressed |
challenge [overload|always] |
site, route | The browser check |
probes on|off |
site | Answers scanner probes with 404, on by default. See Protection |
Plus one handler per block, see Handlers.
route
route <pattern> {
...
}
/x matches exactly, /x/* matches everything under /x/, /x* matches everything starting with /x, and * matches every path. A route always needs a block.
site example.com {
route /health {
respond 200 "ok"
}
route /static/* {
files /var/www/static
}
proxy 127.0.0.1:3000
}
strip
strip
Removes the part of the path the route's pattern matched, before nested routes and the handler see it. It takes no arguments and works only inside a route.
What is removed is the pattern without its trailing * and without a trailing slash. For route /api/* that is /api. The rest of the path keeps its leading slash, and the query string is kept as it is.
With this config:
site example.com {
route /api/* {
strip
proxy 127.0.0.1:3000
}
}
| Client asks for | Backend gets, with strip |
Backend gets, without strip |
|---|---|---|
/api/auth/me |
/auth/me |
/api/auth/me |
/api/users?page=2 |
/users?page=2 |
/api/users?page=2 |
/api/ |
/ |
/api/ |
/API/users |
/users |
/API/users |
/api |
The route does not match | The route does not match |
/api alone does not match /api/*. If clients use it, add an exact route that redirects:
route /api {
redirect /api/ 301
}
The same rule for the other pattern forms:
| Route | Removed | /api/x becomes |
|---|---|---|
route /api/* |
/api |
/x |
route /api* |
/api |
/x, and /apix becomes /x too |
route /api (exact) |
/api |
not matched. /api itself becomes / |
route * |
nothing | /api/x |
Use /api/* rather than /api* with strip, so /api-docs is not turned into /-docs.
When the client's encoding of the prefix matches the pattern, the rest of the path is passed on byte for byte, so /api/v1/a%2Fb arrives as /v1/a%2Fb.
Common mistake
Use strip only when the backend does not know about the prefix. If your backend's own routes already start with /api, for example an Express app with app.get('/api/auth/me') or a Next.js app with a basePath, do not use strip. With strip, the backend gets /auth/me, has no route for it, and answers 404.
# The backend serves /api/... itself: no strip.
route /api/* {
proxy 127.0.0.1:3000
}
# The backend serves /auth/me, /users, ... at its root: strip.
route /api/* {
strip
proxy 127.0.0.1:4000
}
rewrite
rewrite </path[?query]>
Replaces the path. The target must start with /. If it contains a ?, the query string is replaced too. Without a ?, the client's query string is kept. A target ending in ? removes the query string.
| Line | /old?x=1 becomes |
|---|---|
rewrite /new |
/new?x=1 |
rewrite /new?view=lb |
/new?view=lb |
rewrite /new? |
/new |
In a block with both, strip runs first and rewrite second, whatever order you write them in. Nested routes and the handler see the rewritten path. Placeholders are not expanded in rewrite.
A dashboard view chosen by query string:
route /lb/* {
strip
rewrite /?view=lb
proxy 127.0.0.1:9090
}
A single page for every path, as a 404:
site * {
rewrite /index.html
files /var/www/unassigned {
status 404
}
}
header
header <Name> <value>
header -<Name>
Sets a response header, replacing any value the handler gave it, or removes it with a leading -. Quote values that contain spaces. Several header lines in a block are applied in order.
site example.com {
header X-Robots-Tag "noindex, nofollow"
header -X-Debug
route /live.json {
header Cache-Control no-store
files /var/www/live
}
proxy 127.0.0.1:3000
}
header lines apply to whatever answered within the site: the handler, an auth service's reply, the browser check page, and the 404 when no route matched. They do not apply to scanner probe replies or to the 404 for an unknown hostname.
The Server and X-Powered-By headers are set after header lines, on every response. To control them, use the XTQW_SERVER_HEADER and XTQW_POWERED_BY environment variables. See Operations.
header changes the response to the client. To change the request sent to a backend, use header_up in a proxy block.
ip and not ip
ip <entry>...
not ip <entry>...
Limits a route to some client addresses. With ip, the route matches only for clients in the list. With not ip, it matches only for clients outside it. If the address check fails, the route does not match and the next route is tried.
An entry is one of:
- an address:
203.0.113.7,2001:db8::1, - a CIDR range:
10.0.0.0/8,2001:db8::/32, - a file:
@/etc/xtq-web/admins.list.
List every entry on one line. One ip or not ip per route. ip works only inside a route, so the site's other routes stay reachable.
A file holds one address, CIDR range or hostname per line, with # comments:
# office
203.0.113.0/24
# home, dynamic DNS
home.example.com
Hostnames in a file are resolved to every address they have. Files are read again every 60 seconds and hostnames resolved again, so a dynamic DNS name keeps working without a reload. If a later read fails or comes back empty, the previous list is kept.
If a file can't be read when the config loads, xTQ Web logs WARNING: ip list ...; it matches no one until it can be read and keeps going. With ip, that route then matches nobody from that file. With not ip, it matches every client. The next 60-second refresh picks the file up once it is readable. Hostnames can only appear in files, not directly on the ip line.
The address checked is the real client address worked out with trust. If a proxy or CDN is in front of xTQ Web, list it in trust, or every request looks like it comes from the proxy. See HTTPS.
Only a list of addresses may reach an admin area. Everyone else gets a fixed page with status 403:
site admin.example.com {
route * {
not ip 203.0.113.7 10.0.0.0/8 @/etc/xtq-web/admins.list
rewrite /blocked.html
files /var/www/blocked {
status 403
}
}
proxy 127.0.0.1:3000
}
The reverse, an area only for some clients:
route /internal/* {
ip 10.0.0.0/8
proxy 127.0.0.1:9000
}
Clients outside 10.0.0.0/8 skip this route and fall through to the next one or the site's handler.
auth
auth <backend> </path>
Asks another service whether the request may continue, before the handler runs. The backend is written like a proxy backend: 127.0.0.1:8082, http://auth.internal:80 or unix:/run/auth.sock. The path must start with /.
xTQ Web sends a GET to that path on the auth service. The original query string is appended. The request carries the client's headers, cookies and Authorization included, minus Content-Length and Content-Type, with Host and the X-Forwarded-* headers set as for a proxied request. The auth service does not see the original path or method.
| Auth service answers | Result |
|---|---|
Any 2xx |
The request continues to the handler. The auth reply is discarded, headers included |
444 |
The connection is closed without a reply |
| Anything else | That reply, status, headers and body, goes to the client. The handler never runs |
| Not reachable | 502 Bad Gateway |
| No answer within 10 seconds | 504 Gateway Timeout |
header lines of the route are applied to the auth service's reply too.
An auth in an outer block and another in a nested route both run, outer first. Inside one block only the last auth line counts.
A status page behind a login session:
site status.example.com {
route /panel/* {
strip
auth 127.0.0.1:8082 /auth
files /var/www/panel
}
respond 404
}
The service at 127.0.0.1:8082 checks the session cookie and answers 204 to let the visitor in, 302 with a Location to send them to a login page, or 444 to drop a caller it wants to ignore.
compress
compress off
compress on
compress
Proxied responses are compressed by default when the client accepts it and the content is text. compress off turns that off for the block and the routes inside it. compress on, or compress alone, turns it back on in a nested route.
route /downloads/* {
compress off
proxy 127.0.0.1:3000
}
compress affects only the proxy handler. Files and PHP pages have their own rules, see Static files and Handlers. What is compressed and how is in Reverse proxy.
challenge
challenge
challenge overload
challenge always
Puts xTQ Web's browser check in front of the block and every route inside it. A visitor without a valid pass gets a small page that solves a proof-of-work puzzle in the browser, receives a pass cookie and reloads. Clients that don't run JavaScript never get past it.
| Mode | When the check is shown |
|---|---|
challenge or challenge overload |
Only while the backend that would answer is under heavy load. For proxy, that is 75% of the pool's in-flight cap in use, or any request waiting for a slot, and it stays on until use drops below 50%. For php, while xTQ PHP reports overload. files, redirect, respond and abort are never checked in this mode |
challenge always |
Every request without a pass, whatever the handler |
One pass covers the whole site, every route with a challenge. It lasts 24 hours.
site shop.example.com {
route /checkout/* {
challenge
proxy 127.0.0.1:3000
}
proxy 127.0.0.1:3000
}
Only a valid pass cookie lets a visitor through. It works over plain HTTP and HTTPS alike, and is marked Secure when the visitor came over HTTPS. Set XTQW_SECRET so passes stay valid across restarts and reloads. The full description, including thresholds and settings, is in Protection.
probes
probes off
probes on
Scanner probes, paths like /.env, /.git/config or /cgi-bin/..., get a short 404 before any route of the site is looked at. That is probes on, the default. probes off lets them through to the site's routes like any other path, for an app that really serves such a path.
site app.example.com {
probes off
proxy 127.0.0.1:8080
}
probes goes only directly in a site block and covers the whole site. In a route it is an error (probes goes directly inside a site block). Which paths count as probes is in Protection.
