Feature Proposal Description
Middleware registered on a Group or via Use with a path prefix fires for every request matching that prefix, even when no concrete route under it resolves. The 404 only triggers after the entire middleware chain runs.
api := app.Group("/api", AuthMiddleware(), Logger(), RateLimit())
api.Get("/test", handler)
Hitting /api/nonexistent triggers chain of middleware just to return a 404, unecessary db and cache call.
Why it matters at scale
-
- Scanner/bot traffic hitting random paths runs full middleware chains. Auth middleware that touches DB or Redis gets hammered for free.
-
- Rate limiters get polluted by 404 spam — legitimate clients can be throttled.
-
- Observability is noisy: logs, traces, metrics fire for non-existent routes.
-
- Per-route middleware (api.Get("/test", handler, mw)) is the documented workaround but is unmaintainable in codebases with hundreds of routes.
Existing related issues
#1959, #1704, #1179 — all hit variations of this. All have the same workaround suggestion ("add a 404 handler at the bottom") which doesn't actually prevent middleware execution, it just gives you a custom 404 page.
Proposal
Building a route-existence matcher externally and short-circuiting before middleware. I built a radix-tree matcher over app.GetRoutes(true) as a proof of concept:
muzzii:fiber_matcher_test/ $ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: test
cpu: AMD Ryzen 7 9700X 8-Core Processor
BenchmarkTrieLookup-16 17634477 68.73 ns/op 0 B/op 0 allocs/op
BenchmarkTrieMiss-16 29907709 39.87 ns/op 0 B/op 0 allocs/op
PASS
ok test 2.519s
Opt-in config flag, preserves backward compatibility:
app := fiber.New(fiber.Config{
SkipMiddlewareOnUnmatched: true,
})
When enabled, the router does a route-existence check against the registered route stack before executing the middleware chain. If no concrete route matches (method, path), jump straight to the NotFound handler. Group/prefix middleware never runs.
Fiber's internal router already maintains this information — we're using app.GetRoutes(true) externally to mirror it. Doing the check inside the router itself would be cheaper and remove the need for users to maintain a parallel structure.
Alignment with Express API
Express handles this implicitly through its router design. Middleware mounted via app.use('/api', mw) or on an express.Router() only executes when a matching route within that router responds to next() — but more importantly, Express's Router exposes route-matching primitives, and 404 handling is conventionally a final app.use((req, res) => res.status(404).send()) after routes are defined, which still suffers the same chain-execution problem.However, Express ecosystems address this with widely-used packages like express-list-endpoints and built-in router.stack introspection, letting users implement match-first guards trivially. Fiber's app.GetRoutes() is the equivalent primitive — this proposal asks the framework to use it internally for a config-gated early-exit, matching how mature Express apps handle the same problem in practice.If the SkipMiddlewareOnUnmatched flag is too opinionated, the lower-effort alternative — exposing app.HasRoute(method, path) or c.RouteExists() — directly mirrors what Express devs already get from router.stack introspection.
HTTP RFC Standards Compliance
The proposal is fully compliant with HTTP RFC standards:
RFC 9110 §15.5.5 (404 Not Found): returning 404 for (method, path) combinations with no registered handler is the correct semantic. The feature only changes when the 404 is determined (before middleware vs. after), not the response itself.
RFC 9110 §15.5.6 (405 Method Not Allowed): the existing behavior of returning 405 when the path exists for a different method is preserved. The pre-match check distinguishes between "path unknown" (→ 404) and "path known, wrong method" (→ 405), keeping the framework's current method-not-allowed handling intact.
RFC 9110 §9.3.2 (HEAD): implementations MUST treat HEAD identically to GET in terms of route resolution. The pre-match check honors this — a HEAD request matches any registered GET route.
No changes to response headers, status codes, body semantics, caching directives, or any wire-level behavior. This is purely an internal short-circuit optimization.
API Stability
The feature is designed for long-term stability:
Opt-in via config flag — SkipMiddlewareOnUnmatched: false by default means zero behavioral change for existing users. No deprecations, no breaking changes for anyone who doesn't enable it.
No new public types or interfaces — adds one boolean field to fiber.Config. Boolean config flags are the most stable API surface possible; even if internal implementation changes, the flag's meaning ("don't run middleware for unmatched routes") is unambiguous and unlikely to need revision.
Builds on existing primitives — app.GetRoutes() is already public and stable. The internal route stack is already maintained per-request. This is a router-internal optimization, not a new subsystem.
Forward-compatible — if Fiber v4 wants to make this the default, the flag can be deprecated cleanly with a single release cycle. If the alternative (app.HasRoute() / c.RouteExists()) is preferred instead, that method signature is trivial and stable.
No interaction with middleware contracts — handlers, c.Next(), error handling, and middleware ordering all remain identical when a route does match. The only observable change is that middleware doesn't execute when no route matches.
Feature Examples
Current behavior:
app := fiber.New()
api := app.Group("/api",
AuthMiddleware(), // DB lookup
LoggerMiddleware(), // emits log line
RateLimiter(), // increments counter
)
api.Get("/users/:id", getUserHandler)
// Request: GET /api/nonexistent
// → AuthMiddleware runs (DB hit)
// → LoggerMiddleware runs (log noise)
// → RateLimiter runs (counter incremented)
// → 404 returned
Proposed behavior with config flag:
app := fiber.New(fiber.Config{
SkipMiddlewareOnUnmatched: true,
})
api := app.Group("/api",
AuthMiddleware(),
LoggerMiddleware(),
RateLimiter(),
)
api.Get("/users/:id", getUserHandler)
// Request: GET /api/nonexistent
// → no route matches (method, path)
// → 404 returned immediately, NotFound handler invoked
// → AuthMiddleware, LoggerMiddleware, RateLimiter never run
// Request: GET /api/users/abc-123
// → route matches
// → AuthMiddleware → LoggerMiddleware → RateLimiter → getUserHandler (unchanged)
// Request: POST /api/users/abc-123
// → path exists for GET, not POST
// → 405 Method Not Allowed (unchanged behavior)
Checklist:
Feature Proposal Description
Middleware registered on a Group or via Use with a path prefix fires for every request matching that prefix, even when no concrete route under it resolves. The 404 only triggers after the entire middleware chain runs.
Hitting
/api/nonexistenttriggers chain of middleware just to return a 404, unecessary db and cache call.Why it matters at scale
Existing related issues
#1959, #1704, #1179 — all hit variations of this. All have the same workaround suggestion ("add a 404 handler at the bottom") which doesn't actually prevent middleware execution, it just gives you a custom 404 page.
Proposal
Building a route-existence matcher externally and short-circuiting before middleware. I built a radix-tree matcher over app.GetRoutes(true) as a proof of concept:
Opt-in config flag, preserves backward compatibility:
When enabled, the router does a route-existence check against the registered route stack before executing the middleware chain. If no concrete route matches (method, path), jump straight to the NotFound handler. Group/prefix middleware never runs.
Fiber's internal router already maintains this information — we're using app.GetRoutes(true) externally to mirror it. Doing the check inside the router itself would be cheaper and remove the need for users to maintain a parallel structure.
Alignment with Express API
Express handles this implicitly through its router design. Middleware mounted via app.use('/api', mw) or on an express.Router() only executes when a matching route within that router responds to next() — but more importantly, Express's Router exposes route-matching primitives, and 404 handling is conventionally a final app.use((req, res) => res.status(404).send()) after routes are defined, which still suffers the same chain-execution problem.However, Express ecosystems address this with widely-used packages like express-list-endpoints and built-in router.stack introspection, letting users implement match-first guards trivially. Fiber's app.GetRoutes() is the equivalent primitive — this proposal asks the framework to use it internally for a config-gated early-exit, matching how mature Express apps handle the same problem in practice.If the SkipMiddlewareOnUnmatched flag is too opinionated, the lower-effort alternative — exposing app.HasRoute(method, path) or c.RouteExists() — directly mirrors what Express devs already get from router.stack introspection.
HTTP RFC Standards Compliance
The proposal is fully compliant with HTTP RFC standards:
RFC 9110 §15.5.5 (404 Not Found): returning 404 for (method, path) combinations with no registered handler is the correct semantic. The feature only changes when the 404 is determined (before middleware vs. after), not the response itself.
RFC 9110 §15.5.6 (405 Method Not Allowed): the existing behavior of returning 405 when the path exists for a different method is preserved. The pre-match check distinguishes between "path unknown" (→ 404) and "path known, wrong method" (→ 405), keeping the framework's current method-not-allowed handling intact.
RFC 9110 §9.3.2 (HEAD): implementations MUST treat HEAD identically to GET in terms of route resolution. The pre-match check honors this — a HEAD request matches any registered GET route.
No changes to response headers, status codes, body semantics, caching directives, or any wire-level behavior. This is purely an internal short-circuit optimization.
API Stability
The feature is designed for long-term stability:
Opt-in via config flag — SkipMiddlewareOnUnmatched: false by default means zero behavioral change for existing users. No deprecations, no breaking changes for anyone who doesn't enable it.
No new public types or interfaces — adds one boolean field to fiber.Config. Boolean config flags are the most stable API surface possible; even if internal implementation changes, the flag's meaning ("don't run middleware for unmatched routes") is unambiguous and unlikely to need revision.
Builds on existing primitives — app.GetRoutes() is already public and stable. The internal route stack is already maintained per-request. This is a router-internal optimization, not a new subsystem.
Forward-compatible — if Fiber v4 wants to make this the default, the flag can be deprecated cleanly with a single release cycle. If the alternative (app.HasRoute() / c.RouteExists()) is preferred instead, that method signature is trivial and stable.
No interaction with middleware contracts — handlers, c.Next(), error handling, and middleware ordering all remain identical when a route does match. The only observable change is that middleware doesn't execute when no route matches.
Feature Examples
Checklist: