skip to content

On an API where every route requires a credential, does a request to an unregistered path answer 401 or not-found?

level: juniorimportance: should knowfreq 42%

answer

  1. where the check is mounted decides
  2. before routing or after matching
  3. a global check hides your path list
  4. exclusions become the real policy

basics

~20 s

It depends where the credential check is mounted. Mounted globally, ahead of route resolution, it answers 401 for any path; attached per route, it never runs for an unregistered path and the built-in not-found response wins.

solid answer

~50 s

Both answers are correct implementations; they differ in which stage sees the request first. A hook mounted for the whole application typically runs before the router, so an unregistered path is refused with `401` exactly like a registered one - an anonymous scanner learns nothing about which paths exist. A check attached to individual routes or route groups only runs once a route has matched, so an unregistered path falls through to the built-in not-found handler with no credential check at all, which quietly tells a scanner which paths are real. Global mounting is the quieter default, but it also refuses health probes, metrics scraping, well-known documents, static assets and the login endpoint itself, so it needs an exclusion list - and that exclusion list becomes the part of the configuration that most deserves review.

go deeper

for a junior

Remember that the answer depends on placement: a credential check registered for the whole application runs even for paths that match nothing, so 401 on a typo URL is expected behaviour.

for a middle

Explain the two mounting styles and their observable difference, and why the global one gives a scanner no way to separate registered paths from unregistered ones.

for a senior

Talk about the exceptions: probes, metrics, assets and token endpoints all need anonymous access, and an over-broad exclusion prefix silently unprotects routes added later.

for a principal

Decide it once for the fleet. A uniform answer for unknown paths is only worth having if every service behaves the same way, so it belongs in a shared template rather than in each team's judgement.

This looks like a trivia question and is actually a direct probe of whether the candidate understands that the response to a request is produced by the first stage that refuses it. ## Two places to mount the check - **Application-wide.** The credential check is registered once for the whole application. In most frameworks a hook registered this way sits ahead of route resolution in the chain, so it runs for every inbound request, including ones whose path matches no registered route. - **Per route or route group.** The check is attached to specific routes, groups or prefixes. It is part of what the router dispatches to, so it can only run after a route has matched. Neither is wrong. They produce different observable behaviour at the edges, and the difference is entirely about ordering. ## What the caller sees | Situation | Global check, ahead of routing | Per-route check | |---|---|---| | Registered path, no credential | `401` | `401` | | Registered path, valid credential, no rights | `403` | `403` | | Unregistered path, no credential | `401` | built-in not-found | | Unregistered path, valid credential | built-in not-found | built-in not-found | The third row is the whole question. It also explains a support ticket shape that confuses people: a typo in a URL answering `401` rather than not-found, which looks like a bug and is not one. ## Why global-first is the quieter shape - Registered and unregistered paths answer identically to an anonymous caller, so scanning a service for its route list returns no signal. - The refusal is cheap: no routing, no binding, no handler, no data access for traffic that has no credential at all. - One registration is easier to reason about than a rule repeated on dozens of route definitions, where the failure mode is the one route somebody forgot. - The refusal is identical whether the path is wrong, the route was removed last release, or the route is one an internal client was never meant to discover. ## What global-first costs 1. **Infrastructure endpoints break.** Liveness and readiness probes, metrics scraping and similar unauthenticated callers start receiving `401` and the platform declares the service unhealthy. 2. **Public surfaces break.** Static assets, documentation, well-known documents and any landing page served by the same process. 3. **The credential-issuing endpoints break** if you forget them: a login or token endpoint cannot itself require a credential. 4. **Error pages** rendered by the framework may themselves route through the chain, depending on how the framework re-enters after a failure. ## The exclusion list is the real policy Every one of those costs is paid with an exception, and the set of exceptions is where the security of the whole arrangement now lives: - prefer exact paths over prefixes; an excluded prefix silently unprotects everything added under it later; - match on the method as well as the path where it matters; - keep the list short enough to review, and review it when routes are added; - remember that an excluded path is reachable by anyone who can reach the service, so it must be safe to serve anonymously on its own merits. ## Finding out which behaviour you actually have Send an anonymous request to a path you are certain was never registered and read the status. That is faster and more reliable than reasoning from the registration code, because frameworks differ in whether an application-wide hook runs before or after route resolution, and because an exclusion pattern can already be catching more than its author intended. The same probe with a valid credential tells you whether the not-found answer is reachable at all. ## The precedence point underneath Whichever behaviour you have, the reasoning is identical: the stage that runs first is the stage that answers. Moving one registration from the application to a route group changes the status an unregistered path returns without touching a single line of error-handling code. That is the whole lesson - failure precedence is configured by placement, not by choosing status codes. It also generalises past this one case. The same placement argument explains why an ownership rule cannot run before the record is loaded, why a body-validation failure can reach a caller who was never going to be allowed in, and why two services with the same rules can still disclose different things. In each case nobody chose the precedence; the chain did, and the chain is what you edit when the answer is wrong. When you are asked this question, say which behaviour you would pick, then say what you would change to get it - the second half is what is actually being probed.

  • Why does an application-wide credential check make path enumeration harder?
    Because registered and unregistered paths answer the same way to an anonymous caller, so a scanner reading status codes learns nothing about the route list. The only paths it can still identify are the ones deliberately excluded from the check, which is why that exclusion list should stay short and exact.
  • What breaks first when the credential check is placed ahead of routing?
    The unauthenticated infrastructure callers: liveness and readiness probes, metrics scraping, well-known documents, static assets, and the endpoint that issues credentials in the first place. Each needs an explicit exception, and each exception is anonymously reachable, so it has to be safe to serve on its own.

saying these in an interview costs you the question

  • Assumes an unregistered path always answers not-found whatever the pipeline does
  • Thinks a per-route check also covers paths that match no route
  • Treats a 401 on a nonexistent path as a bug to be fixed
  • Excludes a whole path prefix from the check to unblock a health probe
  • Believes route resolution always runs before every registered hook