-
Notifications
You must be signed in to change notification settings - Fork 0
Startup Validation
type RouteValidator interface {
ValidateRoutes(routes []RouteInfo) error
}Implement it on the extension itself. The framework type-asserts every registered extension for it; there is nothing to register.
func (e *SecurityExtension) ValidateRoutes(routes []rextension.RouteInfo) error {
var problems []error
for _, ri := range routes {
sec, ok := ri.Route.(rextension.SecuredRouteAccessor)
if !ok {
continue
}
for _, name := range sec.RequiredSchemes() {
if _, found := e.registry.Lookup(name); !found {
problems = append(problems, fmt.Errorf(
"%s %s requires scheme %q, which is not registered",
ri.Route.Method(), ri.Route.Path(), name))
}
}
}
return errors.Join(problems...)
}Exactly once, after every extension's OnInitialize and OnStart have
returned, after every route has been registered on its router — and before any
route table is frozen and before any listener binds.
A non-nil error aborts startup and is returned from Run, wrapped with the
validator's type.
That moment is the only one that works:
-
Not earlier. At
OnInitializethe routes do not all exist yet, and an extension cannot see another extension's routes at all. -
Not later. By
OnReadythe listeners are already accepting.
Making a misconfiguration a deployment failure rather than a runtime surprise. Two cases from this ecosystem, both previously undetectable at startup:
- A route requiring a security scheme the application never registered. That produced a 500 on every request to the route, discoverable only by making one.
- A route declaring roles for a scheme that cannot enforce them. That was served to every authenticated caller, with no diagnostic anywhere — an endpoint marked "admin only" that was not.
Every validator runs, even after one fails, and the errors are joined. Do the same inside your own validator: a startup error naming one of three misconfigured routes gets fixed one deployment at a time, while one naming all three gets fixed once.
var problems []error
for _, ri := range routes {
if err := e.check(ri); err != nil {
problems = append(problems, err)
}
}
return errors.Join(problems...)ValidateRoutes is a read. The tables are not frozen yet, but registering from
here is out of contract — the routes slice has already been assembled, so
anything added would be validated by nobody and, depending on ordering, may not
be seen by other validators.
Declare in OnInitialize or OnStart. Check here.
type RouteInfo struct {
Route route.Route
Router string // which router serves it
BaseURL string // that router's normalised prefix
}Every router's routes, in a stable order, each carrying the router it belongs to.
This is handed over directly rather than discovered through the
router.route.registeredevent, because that event is delivered asynchronously — an extension collecting routes from it raced this synchronous validation pass and could see any subset of them.
rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem