Skip to content

Startup Validation

wiki edited this page Sep 4, 2026 · 1 revision

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...)
}

When it runs

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 OnInitialize the routes do not all exist yet, and an extension cannot see another extension's routes at all.
  • Not later. By OnReady the listeners are already accepting.

What it is for

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.

Report everything, not the first thing

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

Validate, do not mutate

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.

What you receive

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.registered event, because that event is delivered asynchronously — an extension collecting routes from it raced this synchronous validation pass and could see any subset of them.

Clone this wiki locally