fix(routers): detect routes registered outside main() for gin's four siblings - #35
Merged
Conversation
…name A HandleFunc/Handle call counted as a route only when its receiver was a local variable the same function had assigned from http.NewServeMux(). Name tracking cannot see a mux that arrives any other way, so a service that builds its mux in main() and hands it to routes.Register(mux) — or keeps it in a struct field — produced nothing for those registrations, even though the stdlib extractor already walks every function. Check the receiver's type instead. The package-level http.HandleFunc case stays, now confirmed through types.Info rather than by matching the identifier text "http", so a variable of that name cannot pose as the package. Type-checking is also stricter than the name matching it replaces: Handle is a common method name, and only a *http.ServeMux receiver makes it a route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The index chi uses to find route-registering functions and expand their call sites is not chi-specific: every router whose routes live outside main() needs the same two things — a way to find registration functions regardless of what their signature says, and a way to expand a call to one so the prefix and middleware chain at the call site flow into the routes it registers. Move it to registry.go behind routerIndexSpec, which asks each framework only for what differs: which packages are in scope, what a registration looks like, and what its router type is. No behavior change; chi is the only caller so far. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same defect as chi: a function was walked only when it was main/init or its
signature named *mux.Router, so a factory returning http.Handler and a method
registering on a struct field were both invisible. A service that keeps its
routes in a routes package saw only whatever main() registered directly.
Index every function whose body calls a mux.Router method on a router value,
and expand calls to registration functions inline. A subrouter handed across
a package boundary — routes.RegisterItems(api) where api came from
r.PathPrefix("/api/v1").Subrouter() — now carries its prefix into the routes
that registrar adds; registrars nothing reached still emit, with the
unknown-origin caveat.
Registrations were matched on method name alone, with no check on the
receiver. That was survivable while only main() was walked; with every
route-setup function in scope it would turn any bus.Handle("topic", fn) into
a route, so the receiver is now confirmed to be a mux.Router. isGorillaMuxType
compares the owning package path rather than searching the printed type for
"mux.Router", which only worked because gorilla/mux carries no version
suffix — the exact trap that broke chi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both extractors returned nothing at all for a service that keeps its routes
in a routes package, because both defects were present at once.
A function was walked only when it was main/init or its signature named the
router type, so a factory returning http.Handler and a method registering on
a struct field were never visited. Index every function whose body calls a
registration method on an Echo/App or group value instead, and expand calls
to registration functions inline: a group handed across a package boundary —
routes.RegisterItems(v1) where v1 came from e.Group("/api/v1") — now carries
its prefix and middleware into the routes that registrar adds.
Even when a function was walked, a registration counted only if its receiver
was a local variable the same function had assigned from echo.New() /
fiber.New(). Name tracking cannot see a router that arrives as a parameter,
which is the shape of every routes.Register(e) helper, so echo found nothing
whatsoever and fiber found only what a factory built inline. Receivers are
identified by type now; group variables keep their own map, since a group's
prefix is what distinguishes it from the instance it was carved from.
Fiber's group parameter type is the fiber.Router interface, not *fiber.Group,
so both are accepted. Middleware chains became []MiddlewareRef throughout,
so a chain assembled in one package and applied in another stays resolvable.
The two extractors are near-identical in shape and their tests share a file,
so they land together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
isEchoType and isFiberAppType lost their last callers when the signature gates went away; the broader isEchoRouterType / isFiberRouterType cover every remaining use. Note in the README that route discovery no longer depends on where a project registers its routes, for every router. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #33 / #34 (now merged). Chi was not alone.
I tested three "routes outside
main()" shapes against every router — a cross-packageroutes.Register(r)helper, a factory that builds its own router, and a method registering on a struct field:Echo and fiber returned a completely empty table for the reporter's layout — the same symptom as the original issue. Gin was the only router already correct, because it walks every function and keys on types.
Two independent defects
1. The function gate chi had. Echo, fiber and gorilla walked a function only if it was
main/initor its signature named the router type. A factory returninghttp.Handleroranyis never walked.2. Receivers recognized by variable name, not type. Echo, fiber and stdlib accepted a registration only when the receiver was a local variable assigned from
echo.New()/fiber.New()/http.NewServeMux(). A router arriving as a parameter — the shape of everyroutes.Register(r)helper — or held in a struct field was invisible even when the function was walked.Gorilla had only #1, stdlib only #2, echo and fiber both.
What changed
registry.go(first commit, no behavior change): the index chi uses to find registration functions and expand their call sites is not chi-specific, so it moves behindrouterIndexSpec, which asks each framework only for what differs — packages in scope, what a registration looks like, its router type. All five extractors now share one implementation.http.HandleFunccase stays, now confirmed throughtypes.Inforather than by matching the identifier text"http".routes.RegisterItems(api)whereapicame fromr.PathPrefix("/api/v1").Subrouter()) carries its prefix.isGorillaMuxTypenow compares the package path instead of searching the printed type for"mux.Router"— that only worked because gorilla/mux carries no version suffix, the exact trap that broke chi.fiber.Routerinterface, not*fiber.Group, so both are accepted.Middleware chains became
[]MiddlewareRefin gorilla, echo and fiber, so a chain assembled in one package and applied in another stays resolvable.Safety
Widening the walk means unrelated methods sharing a registration's name come into scope — gorilla matched
HandleFunc/Handleon name alone with no receiver check at all. Every extractor now confirms the receiver's type, and each new testdata module contains a decoy (bus.Handle("warm", nil),bus.Any(…),bus.All(…)) asserted not to appear.Verification
Four new testdata modules —
stdlib-multipkg,gorilla-multipkg,echo-multipkg,fiber-multipkg— each mirroring a real service layout, all six routes resolving with correct prefixes. The per-router assertions share one helper (assertRoutesOutsideMain), which also checks no route is emitted twice and none carries an unresolved caveat; the chi and stdlib tests were folded onto it rather than copied a third and fourth time.go build,go vetandgo test ./...pass. Benchmarks unmoved — extraction is a few ms against ~150ms of package loading.🤖 Generated with Claude Code