# 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.
