# Netlify _redirects rules that silently never match
## When this applies
You deployed a site with redirect rules. The deploy log says "All redirect rules deployed without errors." But the URL does not redirect, does not rewrite, or a rule you expected to fire is never reached. The three usual causes are: the rule sits below an earlier rule that already matched, a real file shadows the rule, or the status code means something different from what you assumed.
## Rule processing order: first match wins
The redirects engine reads rules from top to bottom and applies the first rule that matches a request. It does not continue looking for a better match. Rules in the `_redirects` file are always processed before rules in `netlify.toml` — so a catch-all in `_redirects` will swallow rules that exist only in `netlify.toml`.
Dead rule, `_redirects` version:
```
# This fires for /jobs/customer-ninja-rockstar
/jobs/customer-ninja-rockstar /careers/support-engineer
# This fires for everything else under /jobs
/jobs/* /careers/:splat
# DEAD: the rule above already matched this path
/jobs/outdated-job-link /careers/position-filled
```
Fix: list more specific rules before more general ones. The single most common instance is the SPA catch-all placed too early:
```
# A rule for /jobs/* placed here will never fire
/* /index.html 200
# SPA catch-all goes LAST
/jobs/* /careers/:splat
/* /index.html 200
```
## Splats and placeholders
A splat (`*`) matches anything that follows it. The matched portion is referenced in the target as `:splat`:
```
# _redirects
/news/* /blog/:splat
```
This turns `/news/2004/01/10/my-story` into `/blog/2004/01/10/my-story`. Two hard limits: a splat can only appear at the end of the path (`/jobs/*` works, `/jobs/*.html` does not), and there is no "exclude this path" syntax — exclusion is done by placing a more specific rule above the splat rule.
A placeholder (`:month`, `:year`, ...) matches one path segment, from one `/` to the next, or a final segment including its file extension:
```
# _redirects
/news/:month/:date/:year/:slug /blog/:year/:month/:date/:slug
```
`/news/02/12/2004/my-story` becomes `/blog/2004/02/12/my-story`.
Note: edge nodes normalize URLs before rules run, so a rule matches regardless of a trailing slash. A rule from `/blog/typo` also matches `/blog/typo/`. Do not write rules that try to add or remove a trailing slash; they will not behave as written.
netlify.toml equivalents:
```toml
[[redirects]]
from = "/news/*"
to = "/blog/:splat"
[[redirects]]
from = "/news/:month/:date/:year/:slug"
to = "/blog/:year/:month/:date/:slug"
```
## 200 rewrites vs 301/302 redirects
The status code is the difference between a rewrite and a redirect:
- `301` (the default when no code is given): permanent redirect. The URL in the browser address bar changes to the target.
- `302`: temporary redirect. The URL in the browser address bar also changes.
- `200`: rewrite. The URL in the address bar stays the same; Netlify fetches the target content behind the scenes. This is how SPA catch-alls and same-origin API proxies work:
```
# _redirects
/* /index.html 200
/api/* https://api.example.com/:splat 200
```
A 200 rewrite to an external service is a proxy: the browser never connects to the external host directly.
netlify.toml equivalents:
```toml
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[redirects]]
from = "/api/*"
to = "https://api.example.com/:splat"
status = 200
```
## Forced shadowing with `!`
By default a real file wins over a redirect rule. With only this rule:
```
/* /index.html 200
```
a request for `/partials/chat.html` still serves the actual file, because the file shadows the rule. This is the preferred behavior for SPAs, but it also silently defeats rules you expected to fire when a same-named file exists.
To force the rule to apply even when a file matches the URL, append `!` to the status code in `_redirects`, or set `force = true` in `netlify.toml`:
```
# _redirects
/app/* /app/index.html 200!
/best-pets/dogs /best-pets/cats.html 200!
```
```toml
[[redirects]]
from = "/app/*"
to = "/app/index.html"
status = 200
force = true
[[redirects]]
from = "/best-pets/dogs"
to = "/best-pets/cats.html"
status = 200
force = true
```
With the forced rule, `/best-pets/dogs` always serves the content of `/best-pets/cats.html`, even if `/best-pets/dogs/index.html` exists. Rule of thumb: use `!` only when you are certain the file should never be served directly.
## Troubleshooting checklist
1. Reorder: put the SPA catch-all (`/* /index.html 200`) last; put specific rules above splat rules.
2. Check for a same-named file: if a file exists at the source path, the rule needs `!` (or `force = true`).
3. Check which file the rule is in: a `_redirects` rule always runs before any `netlify.toml` rule.
4. Check the status code: 301/302 change the address bar, 200 keeps it; the default is 301 when the code is omitted.
5. Check splat placement: `*` must be at the end of the path segment — `/jobs/*.html` never works.
6. Watch for loops: never point a rule's target at a path that matches the rule's own source.