std/router
import std/router to use it. Source: src/std/router.iyi at v0.18.0.
A pattern is [METHOD ][HOST]/[PATH]. A method matches exactly, except that GET matches HEAD too; no method matches every method, no host every host. A path is segments: a literal matches the request's segment percent-decoded (/%61 matches /a, /a%2fb matches one segment a/b); {name} matches one non-empty segment; a last {name...} matches the rest of the path, slashes and nothing included; a trailing / is an anonymous one, a subtree; {$} matches only the end, so /{$} is / alone where / is every path. A wildcard's value is percent-decoded and handed to the handler by name in a Hash(String, String); a trailing / records nothing.
When several patterns match a request the most specific wins: the one whose requests are a strict subset of the other's. Two patterns neither of which is more specific, but which both match some request, conflict, and the second is refused when it is registered, with a sentence naming both and a request both match: GET / and /index.html conflict (one has the narrower method, the other the narrower path), as do /a/{x} and /{y}/b. A pattern with a host beats one without, whatever their paths, and the two never conflict - Go's one exception, kept for its reasons. Precedence is decided by comparing patterns, not by the order they were registered in, so a table means the same in any order.
What no pattern answers: a path that matches only patterns of other methods - itself, or with a slash added - is 405 with Allow listing them, sorted, HEAD beside GET; a request for /x where /x/ (or /x/{rest...}) is registered and /x is not is redirected to /x/ (301, the query kept); a path with a . or .. segment or a repeated slash is redirected to its cleaned form (301), as Go's is, so a handler is only ever handed the path its pattern matched; anything else is 404. An escaped %2e%2e or %2F is not a dot segment or a separator, and arrives in a wildcard's value as .. or /: a handler that turns a value into a file name checks it (std/fileserver does its own).
Where this departs from Go, and why: a path with a malformed percent escape is 400 here - Go's server refuses it before its mux sees it, and std/http hands it on. A pattern holding one, or whose path is not clean (//, ., ..), is refused where Go 1.25 takes it as written: no request reaches it, cleaned and with its escapes valid. A target that is not a path (OPTIONS *, CONNECT's authority) is 404. Wildcard names are ASCII identifiers (Go takes any Unicode letter). The Host is matched as Go matches it: its port dropped, otherwise as sent. bench/std_router_exercise.sh hands its tables - 70 requests, 23 pairs of patterns registered in turn, 28 patterns read - to Go 1.25's own ServeMux where a Go toolchain is installed, and every answer is the same.
Patterns are registered before the router answers: a handle after its first call panics. Go takes them at any time under a lock; refusing them means the table a request is matched against is never written while a fiber reads it, and that whether two patterns conflict is known before any request depends on which wins. Each request is compared with every pattern of its host, which is linear in the table; there is no tree, since tables are tens of routes and the comparison that picks the winner is the same one that refuses a conflict, which keeps the two from disagreeing. Measured, a release build with the gate's first table (21 patterns) routed 200,000 requests in 165 ms, under a microsecond each.
Patternclass
A route's pattern, [METHOD ][HOST]/[PATH], parsed: what Router#handle registers. match reads a request path's wildcards; relation and conflict are the comparisons that decide precedence and refuse two patterns neither of which is more specific.
pub class Pattern
# Two relations of parts (methods, then each segment) as the relation
# of the whole: a superset in one part and a subset in another is
# neither, an overlap.
def self.combine(first : Int32, second : Int32) : Int32
# The sentence refusing this pattern beside *other*, nil when the two
# can be registered together: when they have the same host and match
# the same requests, or some requests in common with neither more
# specific. It names both and a request both match.
def conflict(other : Pattern) : String | ::Nil
# Whether this pattern matched *pieces* without its trailing wildcard
# taking anything: `/x/` on `/x/`, not on `/x/y`. What decides whether
# `/x` is redirected to `/x/`.
def exact?(pieces : Array(String)) : Bool
def host : String
# A wildcard's name: an ASCII letter or `_`, then letters, digits, `_`.
def self.identifier?(name : String) : Bool
def initialize(text : String, method : String, host : String, kinds : Array(Int32), names : Array(String))
protected def kinds : Array(Int32)
# The wildcards' values when *path* - a request path, rooted and clean,
# still percent-encoded - matches this pattern's path; nil when it does
# not. The method and host are not looked at.
def match(path : String) : Hash(String, String) | ::Nil
# `match` on a path already cut into `pieces`.
def match_pieces(pieces : Array(String)) : Hash(String, String) | ::Nil
# Whether a request of *method* is one this pattern's method takes.
def matches_method?(method : String) : Bool
def method : String
# Whether this pattern matches a strict subset of *other*'s requests,
# methods and paths together; hosts are not compared.
def more_specific?(other : Pattern) : Bool
protected def names : Array(String)
# *text* read as a pattern, or why it is not one: an invalid method, no
# `/` to start the path, a `{` in the host, a path a request's cleaned
# path never equals (`//`, `.`, `..`), a wildcard that is not a whole
# segment, `{$}` or `{name...}` before the end, a name that is not an
# identifier or is used twice, a malformed percent escape.
def self.parse(text : String) : Pattern | RouterError
# *path*'s segments after its first `/`, each percent-decoded: `/` is
# one empty piece, and a trailing slash is a last empty one. Nil for a
# path that is not rooted or holds a malformed escape. Decoded a segment
# at a time, so `%2F` is inside a segment, never between two.
def self.pieces(path : String) : Array(String) | ::Nil
# How this pattern's requests relate to *other*'s, methods and paths
# together: `EQUIVALENT`, `MORE_GENERAL`, `MORE_SPECIFIC`, `DISJOINT` or
# `OVERLAPS`.
def relation(other : Pattern) : Int32
# The pattern as it was written.
def to_s : String
endRouterclass
Requests sent to handlers by pattern, Go 1.22's ServeMux:
router = Router.new
router.handle("GET /users/{id}") { |req, params| Response.new(200, params["id"]) }
router.handle("/static/") { |req, params| FileServer.serve(root, req) }
Server.serve(listener) { |req| router.call(req) }handle panics on a pattern that is not one, on one that conflicts with a pattern already registered, and after the router has answered a request. call answers with the most specific pattern's handler, or a 301 (to the cleaned path, or to a subtree's /), a 405 with Allow, a 400 for a malformed escape, a 404.
pub class Router
# The answer to *req*: its handler's, or the router's own 301, 400, 404
# or 405.
def call(req : Request) : Response
# *path* - rooted - with `.` and empty segments dropped and `..` taking
# the one before it off, its trailing slash kept: Go's `cleanPath`.
def self.clean(path : String) : String
# Sends the requests *pattern* matches to the block, with the request
# and the wildcards' values by name.
def handle(pattern : String, &handler : (Request, Hash(String, String) -> Response)) : Nil
# A `Host` value without its port, as Go's `stripHostPort` has it: an
# IPv6 address in brackets loses the brackets with the port.
def self.host_of(value : String) : String
def initialize
# The patterns registered, in the order they were.
def patterns : Array(Pattern)
def self.plain(status : Int32, text : String) : Response
def self.redirect(path : String, query : String | ::Nil) : Response
endRouterErrorstruct
Why a pattern is refused: Pattern.parse's answer for text that is not a pattern. Router#handle panics with its message.
pub struct RouterError
def initialize(reason : String)
def reason : String
def to_s : String
endError for RouterErrorimpl
impl Error for RouterError
def message : String
end
A request router for std/http:
Router#handle(pattern) { |req, params| Response }registers a handler, androuter.call(req)is what aServer.serveblock answers with. The patterns, their precedence and the answers when none applies are Go 1.22'shttp.ServeMux, implemented from its documentation (pkg.go.dev/net/http#ServeMux), so a table of routes reads the same in both.