roadrunner_router (roadrunner v0.8.0)
View SourcePath → handler dispatch with parameterized segments.
A route is either a tuple shorthand or a map. Both forms share the
same Path and Handler:
{Path, Handler}— only routes the path; no state, no per-route middlewares.{Path, Handler, State}— adds opaque per-handler state surfaced viaroadrunner_req:state/1.#{path => Path, handler => Handler, state => State, middlewares => [Mw, ...], methods => [~"GET", ...]}— full map form. Use this when you want to attach per-route middlewares, an HTTP-method allowlist, or any future per-route framework knob. Onlypathandhandlerare required; an absentmethodsanswers every method.
The tuple shorthand intentionally cannot carry middlewares — that keeps the simple case syntactically light and pushes "more than just state" to the more verbose map form.
A route may restrict the HTTP methods it answers via the map form's
methods key (a list of uppercase method binaries, e.g.
[~"GET", ~"POST"]). A request whose path matches a route but whose
method is not in that route's list does not match — match/3 keeps
scanning, and if no route on that path accepts the method it returns
{method_not_allowed, Allowed} carrying the union of the methods
declared by the path-matching routes (for a 405 Allow header). A
route with no methods answers every method.
Path is a binary like /users/:id/posts/:post_id. Segments
starting with : capture a single segment into bindings keyed by
the binary name that follows the colon — we deliberately avoid
binary_to_atom/1 on the parsed name to keep the "everything is
binary on the wire" rule we already use for header names.
Segments starting with * (e.g. /static/*path) are wildcard
captures: they consume all remaining path segments and bind them as
a list under the given name. A wildcard must be the last segment in
a pattern; anything after it never matches.
Literal segments must match byte-exactly; comparison is case-sensitive per RFC 3986.
Routes are tried in declaration order — earlier entries win. The
opaque compiled() shape is a list of pre-parsed segment patterns;
swapping to a trie/DAG later is a non-breaking change for callers.
Summary
Types
Captured route parameters, populated by match/3.
The compiled-routes representation match/3 consumes. Treat as
opaque: the shape is an implementation detail and may change.
An HTTP-method allowlist for a route: a list of uppercase method
binaries ([~"GET", ~"POST"]), or undefined to answer every method.
compile/2 turns the list into a #{Method => true} set-map so match
time is an O(1) is_map_key/2 rather than a list scan; methods are
matched byte-exact against roadrunner_req:method/1 (already uppercase
on the wire), so callers must pass uppercase.
A single route entry. Three shapes are accepted
An ordered list of routes; matched first-to-last.
Types
Captured route parameters, populated by match/3.
:param segments produce a single binary value
(#{~"id" => ~"42"}). *wildcard segments produce the list of
remaining path segments
(#{~"rest" => [~"a", ~"b"]}). Empty for routes with no captures.
-opaque compiled()
The compiled-routes representation match/3 consumes. Treat as
opaque: the shape is an implementation detail and may change.
-type methods() :: [binary()] | undefined.
An HTTP-method allowlist for a route: a list of uppercase method
binaries ([~"GET", ~"POST"]), or undefined to answer every method.
compile/2 turns the list into a #{Method => true} set-map so match
time is an O(1) is_map_key/2 rather than a list scan; methods are
matched byte-exact against roadrunner_req:method/1 (already uppercase
on the wire), so callers must pass uppercase.
Matching is literal: a [~"GET"] route does not implicitly answer
HEAD (or any other verb) -- list every method the route accepts.
A present methods must be a non-empty list of binaries; compile/2
raises {invalid_route_methods, _} on an empty list or non-binary
entries (both would otherwise silently reject every request).
-type route() :: {Path :: binary(), Handler :: module()} | {Path :: binary(), Handler :: module(), State :: term()} | #{path := binary(), handler := module(), state => term(), middlewares => roadrunner_middleware:middleware_list(), methods => methods()}.
A single route entry. Three shapes are accepted:
{Path, Handler}— shorthand: no state, no middlewares.{Path, Handler, State}— shorthand with state only.#{path := Path, handler := Handler, state => State, middlewares => Mws, methods => [~"GET", ...]}— map form; use this to attach per-route middlewares, an HTTP-method allowlist, or future per-route framework knobs.
Path is a binary pattern (literal segments, :param captures, or
*wildcard catch-all). Handler is the module implementing
roadrunner_handler. State is opaque per-route data threaded back
to the handler via roadrunner_req:state/1; unset → undefined.
methods is a list of uppercase method binaries the route answers;
unset → every method.
-type routes() :: [route()].
An ordered list of routes; matched first-to-last.
Functions
-spec compile(routes(), roadrunner_middleware:middleware_list()) -> compiled().
Compile a list of routes into the lookup form match/3 expects.
Each path is split on / (empty leading/trailing segments dropped),
and segments starting with : are recorded as named captures.
ListenerMws is the listener-wide middleware list; it is resolved
once (running each module's init/1 a single time) and reused
across every route, composed outermost around each route's own
middlewares (with any per-route state injected before middlewares
run). The conn loop calls the composed fun straight with the request —
zero closure allocations per request. Pass [] for ListenerMws
when compiling routes outside a listener (typically only in tests).
-spec match(Method :: binary(), Path :: binary(), compiled()) -> {ok, module(), bindings(), roadrunner_middleware:next(), term()} | {method_not_allowed, [binary()]} | not_found.
Look up the handler for a given request method + path.
Returns {ok, Handler, Bindings, Pipeline, State} on a match —
Bindings is a map populated with captures from :param segments
(empty for purely literal routes); Pipeline is the pre-composed
next() fun built at compile time (listener mws ++ per-route mws,
optionally wrapped in a state-injecting outermost closure, ending in
fun Handler:handle/1); State is the per-route opaque state
attached by the user at compile time (or undefined when the route
shape didn't carry any). The conn loop just calls Pipeline —
State is for callers who need to introspect a route outside the
request flow.
Method is the uppercase request-method binary. A route with no
methods allowlist answers every method; otherwise the method must be
a member. When a route's path matches but its method does not, the
scan continues (so a later same-path route can answer the method —
that is how same-path method dispatch works). If at least one route's
path matched but none answered the method, returns
{method_not_allowed, Allowed} where Allowed is the sorted,
de-duplicated union of those routes' methods (for a 405 Allow
header). Returns not_found when no compiled route's path matches at
all.