-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
ErrRouterFrozen. Something registered a route or middleware after the route
tables were built.
Almost always an OnReady hook. Move the registration to OnStart. If it is
application code, move it above app.Run().
Registering late is refused rather than raced: the table is published by atomic swap and read without a lock, so there is nowhere safe to add to it.
ErrRouterUnknown. A route or middleware named a router nobody created. The
message carries the call site of the offending registration.
Either the name is a typo, or the extension that creates that router is not
registered. Note that router names are resolved at build time, so registering a
route before its router exists is fine — the router just has to exist by the
time Run reaches step 5.
ErrRouterExists. Two things called CreateRouter with the same name.
If you are writing an extension, this is usually not a failure — reuse the existing router:
if err := r.CreateRouter(name, cfg); err != nil && !errors.Is(err, rex.ErrRouterExists) {
return err
}Be aware that the first creator's configuration wins; the second's is discarded.
Two registrations for the same method and path. Frequently two extensions both
claiming /metrics or /healthz — put one of them on its own router, or
configure its path.
This used to overwrite silently, so the first route vanished with no diagnostic.
In order of likelihood:
-
The registration error was discarded.
RegisterRoutereturns an error. Check it. -
BaseURL. A router withBaseURL: "/api/v1"servesroute.New("GET", "/users", …)at/api/v1/users. Patterns are written without the prefix. -
Wrong router.
RegisterRouteToRouter(rt, "admin")puts it on the admin listener's port, not the default one. -
Trailing slash.
/usersand/users/are different paths.
Call app.Routers()["default"].PrintRootsTree() from an OnReady hook to log
the actual table.
That distinction is now correct: the path existing under other methods gives
405 with an Allow header; the path existing nowhere gives 404.
If you are seeing the old behaviour — 405 for every unmatched method regardless of path — you are running a pre-v0.3.0 router.
If two routes share a prefix and diverge (/a/b/c and /a/{x}/d), matching
requires backtracking, which older versions did not do: the static branch
matched at the shared segment and the parameter branch was never tried, so
/a/b/d 404'd.
Current versions backtrack. If you see this, check the version.
Check which registration it uses. A UsePerRoute factory that
returns nil attaches nothing to that route — which is correct when the
route does not declare the capability the extension looks for.
So: does your route actually implement the interface the extension asks for?
func (r *MyRoute) RequiredSchemes() []string { … } // security
func (r *MyRoute) RequestBody() rextension.BodySchema { … } // validationA pointer receiver means the route must be registered as a pointer for the assertion to succeed.
Order comes from the priority scale, not from
registration order. app.Use(mw) registers at PriorityDefault (1000), which
is innermost — if your middleware needs to run before authentication, register
it with an explicit lower priority.
Returned from Run, synchronously. Binding is no longer done in a goroutine, so
this is an error rather than a log line while the application claims to be
running.
Any listener that bound before the failure is stopped, so nothing is left serving.
ErrScopedFromRoot. You resolved a scoped dependency from
app.Container() instead of from a request scope.
In a handler use ctx.Resolver(). In a factory, take a rextension.Resolver
parameter and use that — a factory closing over the container resolves from
the root:
app.Container().Scoped(func(r rextension.Resolver) *Repo { … }) // r is the scopeTwo registered types satisfy the interface you asked for. The message names
them. Register one, resolve the concrete type, or use ResolveAll if you
genuinely want all of them.
Resolution refuses to pick rather than choosing by map iteration order, which would resolve differently from one process start to the next.
Run listens for SIGINT and SIGTERM itself. If it seems stuck:
- an
OnStoporOnShutdownhook is blocking pastShutdownTimeout— check whether your hooks respect the context they are given; - a handler is still running and the listener's graceful stop is waiting for it.
Raise ShutdownTimeout, or find the hook. The startup wait and the hook rounds
are both bounded, so it should eventually return with a warning logged.
If it is built on a router.request.handled subscriber, that is expected: the
event bus drops events when its queue is full, and an increment can be delivered
while its matching decrement is not.
Anything that must be exact belongs in middleware, not in an event subscriber. See Events.
At LogLevelDebug every request logs twice. Use LogLevelInfo and put a real
access log in middleware, where it can be formatted and sampled.
-
app.Routers()[name].PrintRootsTree()— the actual route table -
LogLevelDebug— hook progress, per-request status, size and duration -
app.Config()— the effective configuration afterRun
rex — Restful Extended eXperience · MIT · © 2026 Kryovyx · pre-1.0 (alpha), interfaces may change
Getting started
Core
Operating
Ecosystem