Rules
A route’s match is a rule in the Traefik v3 syntax, such as
Host(`app.example.com`) && PathPrefix(`/api`), or a structured object that is turned into
such a rule. This page describes the grammar as ReverseDock implements it since v0.1.
For how routes use rules, see Routing and priority.
Syntax
Section titled “Syntax”A rule is one or more matchers combined with operators:
| Element | Example | Meaning |
|---|---|---|
| Matcher | Host(`example.com`) |
A name and its arguments in parentheses. |
&& |
Host(`a.example.com`) && Method(`GET`) |
Both must match. |
|| |
Host(`a.example.com`) || Host(`b.example.com`) |
At least one must match. |
! |
!PathPrefix(`/admin`) |
Negation. |
( ) |
(Path(`/a`) || Path(`/b`)) && Method(`POST`) |
Grouping. |
! binds tightest, then &&, then ||. Whitespace, including line breaks, is ignored between
tokens. & and | must be doubled.
Arguments are strings in one of two forms:
- Backticks:
`text`. Everything up to the next backtick is taken as it is; there are no escapes. This is the usual form, because it needs no escaping inside YAML double quotes. - Double quotes:
"text", with the escapes\\,\",\n,\rand\t. A raw line break inside the string is an error; write\n.
Matcher names are case-insensitive: Host, host and HOST are the same matcher. Each
matcher takes a fixed number of arguments. As in Traefik v3, Host takes one host; write
Host(`a.example.com`) || Host(`b.example.com`) for two.
Limits: a rule is at most 16 KiB, and parentheses and ! nest at most 32 levels deep.
HTTP matchers
Section titled “HTTP matchers”| Matcher | Arguments | Matches when |
|---|---|---|
Host |
host | The request host equals the host, or fits a *. wildcard. * matches any host. |
HostRegexp |
regex | The regex matches the request host. |
Path |
path | The path equals the argument. |
PathPrefix |
prefix | The path starts with the argument. |
PathRegexp |
regex | The regex matches the path. |
Method |
method | The method equals the argument, compared in upper case. |
Header |
name, value | Any value of the header equals value. |
HeaderRegexp |
name, regex | Any value of the header matches the regex. |
Query |
key, value | Any value of the query parameter equals value. |
Query |
key | The parameter is present with an empty value. |
QueryRegexp |
key, regex | Any value of the parameter matches the regex. |
ClientIP |
IP or CIDR | The resolved client IP is the address or inside the range. |
Host and HostRegexp
Section titled “Host and HostRegexp”The request host is taken from the Host header (or the HTTP/2 :authority) and prepared
before it is compared: the port is removed, a trailing dot is removed, it is lower-cased, and
IPv6 brackets are removed. So Host(`app.example.com`) matches APP.example.com:8443 and
app.example.com..
- An argument is compared the same way: case does not matter, a trailing dot is allowed, and
IPv6 addresses may be written with or without brackets (
Host(`[2001:db8::1]`)orHost(`2001:db8::1`)). Host(`*.example.com`)matches exactly one label in front ofexample.com:a.example.com, but notexample.comand nota.b.example.com.- The argument must not include a port (“a host must not include a port; routing ignores the
port”). Non-ASCII names must be written in punycode (
xn--…). Underscores are allowed, because container names use them. HostRegexpis matched against the prepared host (lower case, no port). It is not anchored:HostRegexp(`example`)matcheswww.example.com. WriteHostRegexp(`^[a-z]+\.example\.com$`)to match whole names.
Path, PathPrefix and PathRegexp
Section titled “Path, PathPrefix and PathRegexp”These match the request path after normalisation, without the query string. They compare case-sensitively.
PathandPathPrefixarguments must start with/.PathPrefixis a plain string prefix:PathPrefix(`/api`)matches/api,/api/usersand also/apiv2. UsePathPrefix(`/api/`)to match only below/api/.PathRegexpis not anchored:PathRegexp(`v[0-9]+`)matches/api/v2/x. Use^and$to match the whole path.
Method
Section titled “Method”The argument is upper-cased, so Method(`get`) matches GET. It must be a valid HTTP
method token.
Header and HeaderRegexp
Section titled “Header and HeaderRegexp”The header name is case-insensitive and must be a valid header name. The value is compared
exactly, including case. When the header appears more than once, the matcher matches if any
one value matches. HeaderRegexp is not anchored.
Query and QueryRegexp
Section titled “Query and QueryRegexp”The query string is decoded as application/x-www-form-urlencoded: %20 and + become a
space. When a parameter appears more than once, any one value may match.
Query(`debug`) with one argument matches only an empty value: ?debug or ?debug=. It does
not match ?debug=1; use QueryRegexp(`debug`, `.*`) to match the parameter with any value.
ClientIP
Section titled “ClientIP”The argument is an IPv4 or IPv6 address or a CIDR range. It is compared with the resolved
client IP: the TCP peer, or the address from X-Forwarded-For / X-Real-IP when the peer is
listed in security.trusted_proxies. See Behind another proxy.
Regular expressions
Section titled “Regular expressions”HostRegexp, PathRegexp, HeaderRegexp and QueryRegexp use the syntax of the Rust regex
crate. They are case-sensitive unless you start them with (?i), are not anchored, and their
compiled size is limited to 1 MiB. Inside a YAML double-quoted string, a backslash must be
doubled ("PathRegexp(`^/v[0-9]+\\.json$`)"), or write the rule in single quotes.
TCP matchers
Section titled “TCP matchers”HostSNI, HostSNIRegexp and ALPN exist for TCP routes, which are planned for v0.6.
The parser and rdock validate check them in tcp_routes, and refuse them in HTTP routes. A
TCP rule may also use ClientIP. ALPN refuses acme-tls/1, which is reserved for ACME
challenges.
Structured match
Section titled “Structured match”Instead of a rule, match can be an object. All given conditions must hold; inside a list, any
item may match:
routes: api: match: hosts: [api.example.com, api.example.org] path_prefix: /v1/ methods: [GET, POST] service: api| Key | Equivalent |
|---|---|
hosts |
Host for each, combined with || |
path |
Path |
path_prefix |
PathPrefix |
path_regex |
PathRegexp |
methods |
Method for each, combined with || |
headers |
Header(name, value) for each entry, combined with && |
query |
Query(key, value) for each entry, combined with && |
client_ips |
ClientIP for each, combined with || |
The example is the same as
(Host(`api.example.com`) || Host(`api.example.org`)) && PathPrefix(`/v1/`) && (Method(`GET`) || Method(`POST`)).
An empty object is an error.
Priority
Section titled “Priority”Each listener checks its routes from the highest priority to the lowest and uses the first route whose rule matches.
- A route’s priority is its
prioritysetting when that is not0. - Otherwise it is the length of the rule in bytes, as written. Longer rules are usually more
specific, so
Host(`app.example.com`) && PathPrefix(`/api`)is checked beforeHost(`app.example.com`). For a structured match, the length of the equivalent rule is used. - Ties go to the route name, in ascending byte order, so the result never depends on the order of the file.
- Explicit and computed priorities compete with each other.
priority: 1makes a catch-all that runs after every other route; a large value such aspriority: 10000puts a route first.
A request that matches no route gets 404. On a plain HTTP listener, if its host has an HTTPS
route, it is redirected to HTTPS instead (defaults.redirect_http_to_https).
Path normalisation
Section titled “Path normalisation”Before routing, ReverseDock normalises the request path. The normalised path is what the backend receives, so the backend sees exactly the path that was routed.
- Percent-escapes of unreserved characters (letters, digits,
-,.,_,~) are decoded, and all other escapes are written in upper case:%7ebecomes~,%2fbecomes%2F. - Dot segments are resolved and empty segments removed:
/./a/../b//cbecomes/b/c. A trailing slash is kept. Escaped dots are decoded first, so/%2e%2e/adminbecomes/admin. - An empty path becomes
/. The asterisk form*(as inOPTIONS *) is kept.
A path with a % not followed by two hex digits, or with a # fragment, is refused with
400.
Matchers then see a decoded form of the normalised path, in which reserved characters and %25
stay encoded:
| Request path | Forwarded to the backend | Seen by Path matchers |
|---|---|---|
/foo%20bar |
/foo%20bar |
/foo bar |
/caf%C3%A9 |
/caf%C3%A9 |
/café |
/a%2Fb |
/a%2Fb |
/a%2Fb |
/100%25 |
/100%25 |
/100%25 |
/%7Euser//x/. |
/~user/x/ |
/~user/x/ |
So PathPrefix(`/foo bar`) matches /foo%20bar, but PathPrefix(`/a/b`) does not match
/a%2Fb: an encoded slash is never a path separator. Encoded slashes are allowed in requests.
If the decoded bytes are not valid UTF-8, matchers see the encoded form.
Path middlewares (strip_prefix, rewrite and the others) work on the normalised path, and a
result that is not itself normalised is refused with 400. See
Redirects and rewrites.
Errors
Section titled “Errors”rdock validate reports rule errors with the route’s path and a caret under the problem:
error: routes.app.match: invalid rule: unknown matcher `Hots`; did you mean `Host`? Hots(`a.example.com`) ^ unknown matcher `Hots`; did you mean `Host`?It also checks each matcher: hosts and IP ranges are well formed, paths start with /, regular
expressions compile, header names are valid, and TCP matchers are not used in HTTP routes.