Routing and priority
A route says which requests it handles (match), which listeners it is on, which middlewares run and which service answers. For each request, ReverseDock picks exactly one route, or none.
How a route is chosen
Section titled “How a route is chosen”- Only the listener’s routes count. A route is attached to the listeners in its
listenersfield, or todefaults.listeners([websecure]unless you change it). A request on another listener never sees it. Routes withenabled: falseare not attached anywhere. - Routes are sorted by priority, highest first; equal priorities are sorted by route name.
- The first route whose rule matches wins. Its middlewares and service handle the request.
- No match: on a plain listener, while
defaults.redirect_http_to_httpsis on, a host that has a route on an HTTPS listener is redirected to HTTPS. Everything else gets the 404 page.
The rules are evaluated against the normalised request: the host in lower case without port or trailing dot, the decoded and normalised path, the method in upper case, the headers, the decoded query parameters and the resolved client IP. The grammar is Traefik v3’s; every matcher is listed in Rules.
Priority is one number
Section titled “Priority is one number”A route’s priority is:
- its
priorityfield, when it is set to a number other than 0, or - the length in bytes of its rule, as Traefik does. For a rule string this is the text after YAML parsing; for a structured
matchit is the length of the equivalent rule.
Explicit and computed priorities are compared with each other: there is no separate tier for routes with a priority. Longer rules are usually more specific, so the default order is usually right:
| Rule | Priority |
|---|---|
PathPrefix(`/`) |
15 |
Host(`app.example.com`) |
23 |
HostRegexp(`^[a-z]+\.example\.com$`) |
36 |
Host(`app.example.com`) && PathPrefix(`/api`) |
45 |
The structured match { hosts: [app.example.com], path_prefix: /api } is the rule Host(`app.example.com`) && PathPrefix(`/api`) and also has priority 45.
When length gives the wrong order
Section titled “When length gives the wrong order”In the table above, the HostRegexp route (36) is tried before the exact Host route (23), so it would take every request for app.example.com that does not go to /api. Lower the broad route rather than raising the specific ones: a raised route also competes with every other computed priority.
listeners: web: { address: ":80" } websecure: { address: ":443", tls: {} }
defaults: certificate: example
certificates: example: cert_file: /etc/rdock/certs/example.pem key_file: /etc/rdock/certs/example.key
routes: app: match: "Host(`app.example.com`)" service: app app-api: match: "Host(`app.example.com`) && PathPrefix(`/api`)" service: api tenants: match: "HostRegexp(`^[a-z]+\\.example\\.com$`)" priority: 20 service: tenants tls: { certificate: example } fallback: match: "PathPrefix(`/`)" priority: 1 service: fallback tls: { certificate: example }
services: app: backends: [{ address: "10.0.0.5:3000" }] api: backends: [{ address: "10.0.0.6:9000" }] tenants: backends: [{ address: "10.0.0.7:8080" }] fallback: backends: [{ address: "10.0.0.8:8080" }]The resulting order on websecure is app-api (45), app (23), tenants (20), fallback (1):
| Request | Route |
|---|---|
https://app.example.com/api/users |
app-api |
https://app.example.com/ |
app |
https://shop.example.com/ |
tenants |
https://other.example.net/ |
fallback |
priority: 1 is the usual way to write a catch-all: it sorts below every rule with a computed priority. Routes without a Host cannot ask for a certificate by name, so tenants and fallback name theirs with tls: { certificate: example }, and defaults.certificate is served to clients whose name no certificate covers. To check which route answered, look at the route field of the access-log line.
Matching details that surprise people
Section titled “Matching details that surprise people”PathPrefixis a plain string prefix.PathPrefix(`/api`)matches/api,/api/usersand also/apiv2. WritePathPrefix(`/api/`)to require the slash, and addPath(`/api`)with||if/apiitself should match.- Regular expressions are unanchored.
PathRegexp(`\.php`)matches/x.php/ytoo; anchor with^and$. They use the Rustregexsyntax (linear time, no look-around, no back-references);(?i)makes them case-insensitive. Hostwildcards cover one label.Host(`*.example.com`)matchesapp.example.com, notexample.comora.b.example.com.Host(`*`)matches any host.- One-argument
Querymatches an empty value.Query(`debug`)matches?debugand?debug=, not?debug=1. For “present with any value” writeQueryRegexp(`debug`, `^.*$`). HeaderandQuerymatch when any value matches, for headers and parameters that appear more than once.- Encoded slashes are not separators.
PathPrefix(`/a/b`)does not match/a%2Fb. Other escapes are decoded for matching, soPathPrefix(`/foo bar`)matches/foo%20bar. ClientIPuses the resolved client IP, not always the TCP peer: withsecurity.trusted_proxiesset, it is the address the trusted proxy reports.- Matcher names are case-insensitive (
pathprefixworks) andMethodcompares in upper case.
Routes on several listeners
Section titled “Routes on several listeners”A route listed on both a plain and an HTTPS listener answers on both, and the plain listener does not redirect its hosts, because the route matches first. To serve a site only over HTTPS, keep its routes on websecure and let the plain listener redirect.
A route meant for plain HTTP only sets listeners: [web] and tls: off. Without tls: off, a route on an HTTPS listener asks for a certificate for its hosts: a certificate under certificates that covers them, or defaults.certificate. Automatic certificates are planned for v0.3.