# Fix Traefik 404s and hijacked routes: rule matching, priority, and PathPrefix traps
Use this when requests get `404 page not found` from Traefik, or when
traffic lands on the wrong service even though every router "looks right".
Traefik picks the winning router by **priority**, and the default priority
is the **length of the rule string** — so a longer, sloppier rule silently
outranks the exact route you meant. Classify the 404 first, then fix the
rule or the priority.
## 1. Classify the 404: no router matched vs wrong router won
Traefik's own response when **no router matches** is a plain
`404 page not found` produced at the entrypoint — no upstream is ever
contacted. That is a different problem from a 404 returned by your app
(right service, wrong path).
- `curl -v` shows `404` with an empty/short body and the access log shows
no backend request → **no router matched**. Check rule syntax and
entrypoint binding (steps 2 and 5).
- The request reaches a service but the wrong one (assets load from the
wrong app, auth headers appear on the wrong host) → a **broader rule
outranked your route**. Check priority and PathPrefix (steps 3 and 4).
- The dashboard (or `GET /api/http/routers`) shows each router's rule and
its computed priority — read that table before changing anything.
## 2. Rule syntax: backticks, v3 vs v2
- Rule values use **backticks**, not single quotes:
`Host(`app.example.com`)`. Single quotes are rejected — the values are
Go string literals.
- `Host(`*.example.com`)` matches exactly **one** subdomain label:
`foo.example.com` yes; `example.com` itself no; `foo.bar.example.com`
no. A bare `Host(`*`)` is a **catch-all** matching every request
regardless of host.
- Traefik v3 parses rules with the **v3 syntax by default**. Old
copy-pasted v2 regexes like ``HostRegexp(`{subdomain:[a-z]+}.example.com`)``
silently stop matching under v3; the v3 equivalent is
``HostRegexp(`[a-z]+\.example\.com`)``. If you must keep v2 rules, set
`ruleSyntax: v2` on that router (dynamic config).
## 3. Priority: longest rule wins unless you say otherwise
> "To avoid path overlap, routes are sorted, by default, in descending
> order using rules length. The priority is directly equal to the length of
> the rule, and so the longest length has the highest priority."
The docs' own example: a router with
``rule: HostRegexp(`[a-z]+\.traefik\.com`)`` (priority **34**) steals
`foobar.traefik.com` from a router with ``rule: Host(`foobar.traefik.com`)``
(priority **26**) — the catch-all regex wins purely by being longer.
The fix is an explicit priority:
```yaml
http:
routers:
catchall:
rule: "HostRegexp(`[a-z]+[.]example[.]com`)"
priority: 1 # stays below everything specific
service: fallback
exact:
rule: "Host(`foobar.example.com`)"
priority: 10 # wins for its own host
service: foobar
```
Rules:
- Set explicit priorities wherever a general rule and a specific rule can
both match. The catch-all gets the lowest number.
- `priority: 0` is **ignored** — it falls back to the rule-length default.
Use 1 (or negative values, which are supported) for a true bottom.
- When two routers from **different providers** tie on priority, the
`providers.precedence` static option breaks the tie (first listed wins).
## 4. PathPrefix is a raw string prefix, not segment-aware
From the docs: ``PathPrefix(`/products`)`` matches `/products`,
`/products/shoes`, `/products/` — **and `/products-for-sale`**.
This is the most common way a new route silently captures a neighbor's
traffic: `PathPrefix(`/rag`)` also matches `/rag-web-api`.
Defenses, in order of preference:
- Prefer a trailing-slash prefix for subtrees:
``PathPrefix(`/rag/`)`` does not match `/rag-web-api`. (Add a second
router with ``Path(`/rag`)`` if the bare path itself must route.)
- Use ``Path(`/exact`)`` for single endpoints.
- Use ``PathRegexp(`^/rag(/|$)`)`` when you need a real segment boundary.
- If two routers must overlap, give the more specific one the higher
explicit `priority` (step 3) instead of hoping rule length saves you.
## 5. Entrypoint binding: the router must listen where the request arrives
A router only sees traffic on its declared `entryPoints`. If the router
isn't in the dashboard's list for the entrypoint the request hits, the
rule never gets evaluated:
```yaml
# labels
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
```
- For HTTPS (`:443`) traffic the router needs TLS enabled
(`tls=true` label / `tls: {}` in file config); a plain-HTTP rule on the
`websecure` entrypoint won't match the TLS handshake.
- A router with no `entryPoints` set listens on **all** entrypoints —
convenient, but also how a debug router accidentally answers production
traffic. Declare entrypoints explicitly.
## 6. Checklist
1. `curl -v`: Traefik's bare `404 page not found` with no backend hit →
no router matched; app's 404 → wrong path/service after routing.
2. Dashboard router table: read the rule **and the computed priority**
for every router on the entrypoint.
3. Longer general rule outranking a specific one → set explicit
`priority` (catch-all lowest; never rely on `priority: 0`).
4. Neighbor traffic captured → `PathPrefix` is a raw string prefix;
use trailing-slash prefixes, `Path()`, or `PathRegexp`.
5. Router not evaluated at all → check `entrypoints` list and `tls`
for `:443`; check backtick quoting and v2-vs-v3 rule syntax.