feat(13-01): register overlapping constrained routes in surf

- compile groups routes ServeMux refuses side by side into overlap families
  and registers each under one generated method-less pattern
- the family handler tries members in registration order on literals and
  Where constraints, sets their path values and runs their own wrapped chain
- no match answers the bare 404; a method mismatch answers ServeMux's 405
  and Allow for the same table without the overlap
- unsupported shapes (same shape, {name...}, shadowing route) fail at boot
- README and docs/services/routing.md describe the behaviour
This commit is contained in:
Jakub Zych
2026-10-03 06:22:20 +02:00
parent 6525d967c5
commit fbdeb20126
5 changed files with 936 additions and 3 deletions

View File

@@ -50,6 +50,19 @@ func showPost(w http.ResponseWriter, r *http.Request) {
}
```
## Overlapping constrained routes
Laravel matches routes in the order `routes.php` declares them and checks `->where()` constraints while it matches. Two routes can therefore share a shape that only their constraints keep apart, for example `GET /shelves/items/{id}` with a numeric `id` and `GET /shelves/{shelfId}/follow` with a numeric `shelfId`. Both patterns match `/shelves/items/follow`, and neither is more specific than the other, so the standard library ServeMux refuses to register them together.
surf accepts such routes. When the application starts, it groups every set of routes that ServeMux would refuse side by side into an overlap family and registers the family once, under a generated pattern with a wildcard wherever its routes differ. A request that reaches the family is tried against its routes in registration order. The first route whose literal segments and constraints match runs, with its own path values, middleware, body limit and panic recovery. Declare overlapping routes in the order the PHP file declares them.
- **No match:** a request whose path no route of the family matches gets the same bare 404 as any unknown path, for every method.
- **Wrong method:** a request whose path a route matches, but not with its method, gets the 405 and `Allow` header that ServeMux gives for the same routes without the overlap.
- **More specific routes** stay outside the family: a literal `GET /shelves/items/similar` keeps answering its own path.
- **Route table:** `surf.Router.Routes` and `route:list` list every route of a family on its own, with its own pattern and middleware.
A family supports literal segments and single-segment `{name}` wildcards only. These shapes still stop the start-up with a route conflict: two routes of one method that differ only in parameter names, a `{name...}` or `{$}` wildcard or a trailing slash in a family, and a more general route of the same method, such as `GET /shelves/{a}/{b}`, that would take a family member's requests.
## Auth groups
An auth group is a group whose middleware list names a guard. The plugin above turns a [bouncer](../../modules/bouncer/README.md) JWT guard into named middleware and returns it from `pact.HasMiddleware`: