Skip to content

Troubleshooting

wiki edited this page Sep 4, 2026 · 1 revision

Troubleshooting

rex: router is already serving; routes and middleware must be registered before Run

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.

rex: unknown router "metrics": route GET /x was registered for it at main.go:42

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.

rex: router already exists

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.

duplicate route GET /metrics (already registered)

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.

My route returns 404 and I am sure it exists

In order of likelihood:

  1. The registration error was discarded. RegisterRoute returns an error. Check it.
  2. BaseURL. A router with BaseURL: "/api/v1" serves route.New("GET", "/users", …) at /api/v1/users. Patterns are written without the prefix.
  3. Wrong router. RegisterRouteToRouter(rt, "admin") puts it on the admin listener's port, not the default one.
  4. Trailing slash. /users and /users/ are different paths.

Call app.Routers()["default"].PrintRootsTree() from an OnReady hook to log the actual table.

I get 405 where I expect 404 (or the reverse)

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.

A route with a path parameter is never matched

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.

An extension's middleware never runs

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 { … } // validation

A pointer receiver means the route must be registered as a pointer for the assertion to succeed.

Middleware runs in the wrong order

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.

listen tcp :8080: bind: address already in use

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.

dix: type is registered as Scoped; resolve it from a Scope

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 scope

dix: ambiguous resolution

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

The application does not stop on Ctrl-C

Run listens for SIGINT and SIGTERM itself. If it seems stuck:

  • an OnStop or OnShutdown hook is blocking past ShutdownTimeout — 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.

My in-flight gauge or counter drifts

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.

Debug logging is too noisy in production

At LogLevelDebug every request logs twice. Use LogLevelInfo and put a real access log in middleware, where it can be formatted and sampled.

Where to look next

  • app.Routers()[name].PrintRootsTree() — the actual route table
  • LogLevelDebug — hook progress, per-request status, size and duration
  • app.Config() — the effective configuration after Run

Clone this wiki locally