Skip to content

Startup Validation

wiki edited this page Sep 4, 2026 · 1 revision

Startup validation

The extension implements rextension.RouteValidator. The framework calls it once, when the route table is built — after every extension has declared, before any listener binds.

A security requirement that cannot be met stops the deployment.

What it checks

A route requires a scheme that is not registered

security: route DELETE /users/{id} requires scheme "jwt", which is not registered

Previously this produced a 500 on every request to that route, discoverable only by making one.

A route declares roles or scopes for a scheme that cannot enforce them

security: route DELETE /users/{id} declares roles for scheme "jwt",
which does not implement RoleEnforcer (or reports SupportsRoles() == false)

This is the serious one. Previously the route was served to every authenticated caller, with no diagnostic anywhere — an endpoint marked "admin only" that was not.

It is not detectable by an interface assertion either: BearerScheme implements RoleValidator, and its implementation returned nil when its inner validator did not know about roles. SupportsRoles is what moves the answer to where it is knowable. See Roles and Scopes.

Roles declared for a scheme the route does not require

A key in RequiredRoles naming a scheme absent from RequiredSchemes is a mistake, not an ignorable entry.

Why here and nowhere else

This is the only point at which every route and every scheme are both known.

  • Not at OnInitialize — the routes do not all exist yet, and an extension cannot see another extension's routes at all.
  • Not at OnReady — the listeners are already accepting.
  • Not per request — a security requirement that cannot be met should stop a deployment, not degrade quietly inside one.

Every problem, not the first

ErrInsecureConfiguration: routes declare requirements the configured schemes
cannot enforce:
  - GET /admin/users requires scheme "jwt", which is not registered
  - DELETE /users/{id} declares roles for scheme "apikey", which cannot enforce them
  - POST /orders declares scopes for scheme "basic", which cannot enforce them

A startup error that names one of five misconfigured routes gets fixed one deployment at a time; one that names five gets fixed once.

The sentinel is security.ErrInsecureConfiguration.

Calling it yourself

err := security.ValidateRoutes(registry, routes)

Useful in a test that asserts your route table is coherent without booting the application:

func TestRoutesAreEnforceable(t *testing.T) {
	reg := security.NewSchemeRegistry(schemes()...)
	if err := security.ValidateRoutes(reg, routes()); err != nil {
		t.Fatal(err)
	}
}

That is worth having: it turns "did anyone remember to register the scheme this new route needs?" into a unit test rather than a deployment.

What it does not check

  • That your validator is correct. It checks that a validator claiming to enforce roles exists, not that it enforces them properly.
  • That a role name is spelled right. Role strings are opaque to the framework.
  • That a route should be secured. A route that declares nothing is public, and that is indistinguishable from a route someone forgot to secure.

For the last one, consider your own RouteValidator — the framework runs every extension's, and your application can be one:

type policyCheck struct{ rex.DefaultExtension }

func (p *policyCheck) ValidateRoutes(routes []rextension.RouteInfo) error {
	var problems []error
	for _, ri := range routes {
		if ri.Router != rex.DefaultRouterName {
			continue // operational listeners are exempt
		}
		sec, ok := ri.Route.(security.SecuredRoute)
		if !ok || len(sec.RequiredSchemes()) == 0 {
			if !isPublic(ri.Route.Path()) {
				problems = append(problems, fmt.Errorf(
					"%s %s is public and not on the allowlist",
					ri.Route.Method(), ri.Route.Path()))
			}
		}
	}
	return errors.Join(problems...)
}

An explicit allowlist of public paths, checked at boot, is the only mechanism that catches "someone added an endpoint and forgot".

Clone this wiki locally