feat(server): fail fast on duplicate route registration - #761
Conversation
Echo's engine hard-codes AllowOverwritingRoute: true, so two modules registering the same method+full path silently collide (last one wins) with no error and no warning. Add a per-Server route conflict tracker threaded through routeGroup/addEcho and sweep it after RegisterRoutes in app.prepareRuntime, aborting startup with an aggregate error naming every collision and both registrants (handler name + caller package). Health/ready probes bypass the registrar and stay excluded; param-name -differing templates (/users/:id vs /users/:uid) are distinct strings and are not detected, matching echo's own radix-tree behavior. Documented in wiki/startup_defaults.md. Skipped a wiki/migrations.md hop atom per the plan's escape hatch: the file's structure requires a known target-version edge, which isn't knowable pre-release.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
WalkthroughHTTP route registrations now track duplicate method-and-path collisions across typed, raw, grouped, nested, and probe routes. Application startup aggregates detected conflicts and fails before maintenance loops begin. Tests and documentation cover the behavior. ChangesDuplicate route conflict detection
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|



Summary
Echo's engine is constructed with
AllowOverwritingRoute: true, so two modules registering the samemethod + full pathsilently collide — last one wins, the first module's handler is dead on arrival, and nothing surfaces unless the shadowed route is exercised. For a modular framework this is exactly the class of bug the Fail Fast principle exists to catch (duplicate module names and duplicate messaging triples already fail startup; HTTP routes did not).This PR adds per-server route-collision tracking at the framework's own registration seam and a startup sweep that aborts with one aggregate error naming every collision and both registrants:
Design decisions
Servertracker, not the globalDefaultRouteRegistry— the registry is a package-level global; dedup there would cross-contaminate parallel tests running multiple servers. The tracker lives onServerand threads through everyrouteGroup(including nestedGroup()children).RouteRegistrar.Addderives provenance locally; the typed path threads the already-built descriptor'sHandlerName/Packagethrough the unexportedechoAdderseam (single implementer, zero blast radius). A sharedaddRoutefunnel was considered and deliberately skipped as non-blocking indirection — the invariant is pinned by tests instead.app.checkRouteConflicts), mirroringapplyGlobalMiddleware— the exportedServerRunnerinterface gains no method, so the apidiff gate stays green and existing test fakes keep compiling. The sweep runs synchronously inprepareRuntimebefore the server starts: a deterministic registration bug fails fast, not as an async post-start crash.New()— they register directly on the engine (not through arouteGroup), soNew()records their four method/path pairs in the tracker; a module claimingGET /healthfails startup like any other collision.HandlerName+ callerPackage—RouteDescriptor.ModuleNameis populated by no registration path (route-log attribution uses registration-order spans), so the error reports what is actually available on both typed and raw paths.AllowOverwritingRoute: false(viable since echo v5.3.1 / Implicitly registered group routes should be allowed overwritten in default routes labstack/echo#3049) — it would require hand-constructing the router, surface as an echo-flavored panic through the ADR-034 boundary, and cannot produce provenance-rich errors./users/:idvs/users/:uid) — distinct strings; echo's radix-tree behavior governs there. Documented inwiki/startup_defaults.md.Adoption note
Apps with a latent duplicate registration will now fail startup with
duplicate route registration …naming both registrants — that is the intended fail-fast on a previously silent bug. Fix by removing or renaming the colliding route. No exported API changed (additive only:Server.RouteConflicts(),server.RouteConflict,server.RouteRegistrant). Awiki/migrations.mdhop atom was deliberately deferred: the file's structure keys on a release-version edge that release-please assigns post-merge; the adopt note lives inwiki/startup_defaults.md#duplicate-route-detection.Testing
TestRouteConflictTrackerRecordsDuplicate).Server(TestServerRouteConflictsAcrossGroups).TestRouteConflictTypedAndRawBothTracked).TestRouteConflictDetectsProbeCollision).TestCheckRouteConflictsAggregatesAndSkips); mutation-verified (removing the record call or neutering the sweep fails the pinning tests).Pre-push gates:
/simplify(2 applied:errors.Joinaggregate matching the in-package idiom; test-table consolidation),/security-audit(clean — gosec 0, no new input surface, error carries compile-time metadata only), CodeRabbit CLI on the final diff (0 findings).make checkgreen after every mutation.Closes #759
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation