skip to content

In a router, a newly added route never runs because an older catch-all serves its path — how do you diagnose and prevent that?

level: seniorimportance: should knowfreq 48%

answer

  1. the table, not the handler
  2. replay the exact path
  3. ask who wins for this path
  4. order versus specificity as causes
  5. narrow the wildcard, assert the winner

basics

~20 s

Replay the exact path, then read the router's effective route table and which template it selects — the handler you are debugging never ran. Fix it by narrowing the catch-all, then pin the winner with a test.

solid answer

~40 s

The handler never ran, so handler logging proves nothing; the evidence is in the router. Replay the request with the exact path — slash, case and escaping included — and check what reached the service, since a proxy may rewrite a prefix. Then dump the **effective route table**: templates, methods, and the order or rank assigned, and ask the router which template it selects for the failing path. The usual causes are a general route winning by position in an order-sensitive router, a specific template that is less specific than it looks, a wildcard consuming separators further than intended, or a route that was never registered. Fix it by narrowing the catch-all to the prefix it truly owns and constraining neighbouring placeholders, then pin it with a test asserting the winning route.

go deeper

for a junior

Recall that a catch-all can absorb paths meant for other routes, and that the first place to look is the router's list of registered routes rather than the handler.

for a middle

Explain the candidate causes and how to separate them: position, rank, wildcard reach, a missing registration, or a path that differs from the one you think you sent.

for a senior

Show a diagnosis you can defend under time pressure — exact replay, table dump, boundary bisect — and fixes that remove the overlap rather than hide it, with a test that pins the winner.

for a principal

Make shadowing unrepresentable: a policy for where catch-alls may be mounted, route tables exported for review, and awareness that a shadowed route may have been the one enforcing authorisation.

## Start by proving what the router actually saw The failing request never reached the handler, so nothing the handler could log will help. The evidence lives one layer earlier, and the first job is to remove guesswork about the input: - Replay the request with the **exact** path, byte for byte: the trailing slash, the capitalisation, any percent-escaped character. A near-miss on any of those is a different diagnosis with the same symptom. - Check what the service received, not what the client sent. A proxy or gateway in front may rewrite, strip or add a prefix, so the path at the router can differ from the path in the address bar. - Confirm the response is coming from the catch-all rather than from a miss: a distinguishing header, a log line, or the shape of the body is usually enough. ## Then read the effective route table Every router builds a table at startup, and the fastest diagnosis is to look at it instead of at the source: - dump the table — route key, template, methods, and the order or rank the router assigned; - if the router can explain a single decision, ask it which template it selects for the failing path; - failing both, bisect: request paths that differ by one segment until the boundary where the catch-all takes over becomes visible. ## The causes, in the order they are usually found 1. **A general route wins by position.** In a first-match router, the catch-all is registered before the specific route, so the scan ends before it is reached. 2. **A general route wins by rank.** In a specificity router, the specific template is less specific than it looks — a placeholder where a literal was intended, or a constraint that the request path fails, so the template is skipped and the catch-all is next in line. 3. **The wildcard reaches further than intended.** A catch-all consumes separators, so a template meant to own one subtree quietly owns everything below the prefix, including paths added later. 4. **The specific route is not in the table at all.** It was never registered, or was registered under a different prefix or group than the one you are reading. 5. **The path is not the path you think.** Slash, case or encoding differences mean the specific template was never a candidate. | Symptom | Most likely cause | |---|---| | Works when the specific route is moved earlier | order-sensitive router, general route first | | Works when a segment is spelled differently | slash, case or encoding mismatch | | Route absent from the table dump | never registered, or registered elsewhere | | Only deep paths fail, shallow ones work | wildcard consuming separators | ## Fixes, best first - **Narrow the catch-all.** Give it the prefix it genuinely owns and nothing more. A wildcard mounted at the root of a service that also serves specific routes is the root cause, and every other fix is a workaround. - **Constrain the placeholders** that sit next to literals, so a keyword segment cannot be swallowed by an identifier route. - **Order specific before general.** Necessary in an order-sensitive router, harmless elsewhere, so it is a cheap house rule. - **Avoid the tempting workaround**: branching inside the catch-all handler to reproduce the specific route's behaviour. That hides the overlap, duplicates the logic and will hide the next one too. ## Guardrails so it does not recur - A test per overlapping pair that asserts **which route handles a representative path**, not merely that the response is a success. This is the only guard that survives someone reordering registrations. - A route-table snapshot in the test suite, so an unexpected new entry or rank change shows up as a diff. - The table exposed at runtime — a log at startup or an operator surface — so the same question can be answered in a deployed environment without a rebuild. - A review rule: a new catch-all needs a stated prefix and a reason. ## The security angle worth raising Shadowing is not only a correctness bug. If the specific route carried an authorisation check and the catch-all does not, requests that should have been rejected are now served by the broader handler. The same applies in reverse: a catch-all that answers everything under a prefix removes the router's ability to report a genuine miss there, so probing for paths that do not exist gets a friendly response instead of a 404. When you find a shadowing bug, check what the shadowed route was enforcing before you close the ticket.

  • What kind of test stops this recurring after a refactor?
    One that asserts which route handles a representative path, not merely that the response succeeded — a success assertion passes happily while the wrong handler serves it. A route-table snapshot alongside it turns an unexpected entry or rank change into a review diff rather than a production discovery.
  • When is a catch-all the right tool at all?
    When a subtree genuinely has no enumerable routes: a client-side application fallback, a proxied or mounted foreign subtree, or an explicit not-found handler. Scope it under a prefix that owns nothing else, and keep it out of namespaces where specific routes are still being added.
  • Why is branching inside the catch-all handler the wrong fix?
    It reproduces the shadowed route's behaviour in a second place while leaving the overlap in the table, so the logic drifts and the next route added under that prefix is shadowed too. It also keeps the router unable to report a genuine miss in the subtree.
  • What is the security angle on a shadowed route?
    If the shadowed route carried an authorisation check that the catch-all does not, requests that should have been rejected are now served by the broader handler. A catch-all also answers paths that do not exist, so probing gets a friendly response where a clear miss would have been correct.

saying these in an interview costs you the question

  • Concludes the route was never deployed without reading the route table
  • Raises the handler's log level even though the handler never runs
  • Duplicates the shadowed route's logic inside the catch-all branch
  • Assumes reordering registrations fixes it in any router
  • Tests only that the response succeeded, never which route matched