net/router
Routing HTTP requests, with parameters, groups and static files.
import net.router
The router maps a method and a path to a handler function. It is the routing layer of the net/http server: you register routes on a Router, hand it to the server with http.Use, and for every request the server calls Dispatch, copies the path parameters into the request context and calls the handler with that context id. Handlers read parameters with http.Param. Shared work such as access logs, CORS and security headers comes from net/http/middleware, which a handler calls first.
A pattern is matched segment by segment:
- a literal segment such as
/notesmust match exactly, byte for byte (so method names and paths are case sensitive); - a segment that starts with
:is a parameter:/books/:idmatches/books/42withidset to42; - a
*matches everything after it, slashes included, without making a parameter.
Most examples below call Dispatch directly instead of starting a server. It is the same lookup the server does, and it makes the rules easy to see and to test. Dispatch returns a Match with matched, the handler fn and the parameters; free it with FreeMatch.
Things that catch people out:
- Register every route before
http.Use. The server keeps its own copy of the router, so a route added afterwards is never seen and answers 404. - Trailing slashes only match parameter and wildcard routes.
/users/:namealso accepts/users/sara/, but the literal route/notesdoes not accept/notes/. - Parameters are not URL-decoded.
/tags/open%20sourcegivesopen%20source; pass it throughhttp.UrlDecode. The query string is already split off and is read withhttp.Query. - There is no automatic 405 and only one static folder per router; both are covered below.
Routes for a REST resource
A resource such as notes usually gets five routes: list and create on the collection, read, replace and delete on one item. Register each method with its own function (Get, Post, Put, Delete, and Patch for partial updates) and read the item id with ParamGet. A path that matches no pattern, or a method with no route, gives matched false, which the server answers with 404.
import net.http
import net.router
func list_notes(ctx: i64): http.Text(ctx, "all notes") end
func create_note(ctx: i64): http.Text(ctx, "created") end
func show_note(ctx: i64): http.Text(ctx, "one note") end
func update_note(ctx: i64): http.Text(ctx, "updated") end
func delete_note(ctx: i64): http.Text(ctx, "deleted") end
func handler_name(fn: i64): str:
if fn == list_notes: ret "list_notes" end
if fn == create_note: ret "create_note" end
if fn == show_note: ret "show_note" end
if fn == update_note: ret "update_note" end
if fn == delete_note: ret "delete_note" end
ret "?"
end
func try_request(r: router.Router, method: str, path: str):
mut m := router.Dispatch(r, method, path)
if m.matched:
id := router.ParamGet(m, "id")
if id.len() > 0:
println method, path, "->", handler_name(m.fn), "with id", id
else:
println method, path, "->", handler_name(m.fn)
end
else:
println method, path, "-> 404"
end
router.FreeMatch(m)
end
func main:
mut r := router.NewRouter()
router.Get(r, "/notes", list_notes)
router.Post(r, "/notes", create_note)
router.Get(r, "/notes/:id", show_note)
router.Put(r, "/notes/:id", update_note)
router.Delete(r, "/notes/:id", delete_note)
try_request(r, "GET", "/notes")
try_request(r, "POST", "/notes")
try_request(r, "GET", "/notes/17")
try_request(r, "PUT", "/notes/17")
try_request(r, "DELETE", "/notes/17")
try_request(r, "DELETE", "/notes")
try_request(r, "GET", "/notes/17/comments")
endA parameter matches exactly one path segment: /notes/17/comments does not reach /notes/:id. Give nested resources their own pattern, such as /notes/:id/comments.
Which route wins
When several patterns fit one path, a literal segment beats a parameter and a parameter beats *, so /users/me reaches its own handler even though /users/:name was registered first. Between two wildcard routes the one registered first wins, so register a catch-all /* last, after more specific ones like /assets/*. The output shows three more rules: /users/sara/ with a trailing slash still reaches the parameter route, /users/me/ misses the literal route and lands on /users/:name with name set to me, and a HEAD request falls back to the GET route.
import net.http
import net.router
func me(ctx: i64): http.Text(ctx, "me") end
func user(ctx: i64): http.Text(ctx, "user") end
func assets(ctx: i64): http.Text(ctx, "assets") end
func page(ctx: i64): http.Text(ctx, "page") end
func handler_name(fn: i64): str:
if fn == me: ret "me" end
if fn == user: ret "user" end
if fn == assets: ret "assets" end
if fn == page: ret "page" end
ret "?"
end
func show(r: router.Router, method: str, path: str):
mut m := router.Dispatch(r, method, path)
if m.matched:
println method, path, "->", handler_name(m.fn), m.params.items.len(), "params"
else:
println method, path, "-> no route"
end
router.FreeMatch(m)
end
func main:
mut r := router.NewRouter()
router.Get(r, "/assets/*", assets)
router.Get(r, "/users/:name", user)
router.Get(r, "/users/me", me)
router.Get(r, "/*", page)
show(r, "GET", "/users/me")
show(r, "GET", "/users/sara")
show(r, "GET", "/users/sara/")
show(r, "GET", "/users/me/")
show(r, "GET", "/assets/css/site.css")
show(r, "GET", "/about")
show(r, "HEAD", "/users/sara")
show(r, "POST", "/users/sara")
end404 or 405: telling the client what is allowed
A request for a path that exists with the wrong method should get 405 Method Not Allowed and an Allow header, not a 404. The router does not do this by itself, but it is easy to build on Dispatch: when a request does not match, try the same path with the other methods and list the ones that do.
import net.http
import net.router
func list_orders(ctx: i64): http.Text(ctx, "orders") end
func create_order(ctx: i64): http.Text(ctx, "created") end
func allowed(r: router.Router, path: str): str:
methods := ["GET", "POST", "PUT", "PATCH", "DELETE"]
mut out := ""
each m in methods:
mut found := router.Dispatch(r, m, path)
if found.matched:
if out.len() > 0: out += ", " end
out += m
end
router.FreeMatch(found)
end
ret out
end
func answer(r: router.Router, method: str, path: str):
mut m := router.Dispatch(r, method, path)
if m.matched:
println method, path, "-> 200"
else:
allow := allowed(r, path)
if allow.len() > 0:
println method, path, "-> 405, Allow:", allow
else:
println method, path, "-> 404"
end
end
router.FreeMatch(m)
end
func main:
mut r := router.NewRouter()
router.Get(r, "/orders", list_orders)
router.Post(r, "/orders", create_order)
answer(r, "GET", "/orders")
answer(r, "DELETE", "/orders")
answer(r, "GET", "/invoices")
endGrouping routes under a prefix
Group registers a route under a shared prefix, which keeps API versions apart: /api/v1/users and /api/v2/users can live side by side and point to different handlers. Keep the prefix in a variable so a version is renamed in one place. The registered routes are in r.routes, which is useful for printing a route table at start-up.
import net.http
import net.router
func users_v1(ctx: i64): http.Json(ctx, "[\"ali\"]") end
func users_v2(ctx: i64): http.Json(ctx, "{\"users\":[\"ali\"]}") end
func user_v2(ctx: i64): http.Json(ctx, "{}") end
func main:
mut r := router.NewRouter()
v1 := "/api/v1"
v2 := "/api/v2"
router.Group(r, v1, "GET", "/users", users_v1)
router.Group(r, v2, "GET", "/users", users_v2)
router.Group(r, v2, "GET", "/users/:id", user_v2)
each route in r.routes:
println route.method, route.pattern
end
mut m := router.Dispatch(r, "GET", "/api/v2/users/5")
println m.fn == user_v2, router.ParamGet(m, "id")
router.FreeMatch(m)
mut old := router.Dispatch(r, "GET", "/api/v1/users")
println old.fn == users_v1
router.FreeMatch(old)
endPath parameters in a running server
Inside a handler the parameters come from the request context: http.Param(ctx, "slug") reads one, and http.Query(ctx, "page") reads the query string, which the server removes from the path before routing. The program below serves a blog and a tag page. It shows the two server rules from the top of the page: the tag arrives still percent-encoded until http.UrlDecode turns it back into text, and /late, registered after http.Use, is a 404.
import net.http
import net.router
func show_tag(ctx: i64):
raw := http.Param(ctx, "tag")
http.Text(ctx, "raw=" + raw + " decoded=" + http.UrlDecode(raw))
end
func show_post(ctx: i64):
http.Json(ctx, "{\"post\":\"" + http.Param(ctx, "slug") + "\",\"page\":\"" + http.Query(ctx, "page") + "\"}")
end
func late(ctx: i64): http.Text(ctx, "late") end
func fetch(path: str):
resp := http.Get("http://127.0.0.1:18579" + path)
println path, resp.status, resp.body
end
func main:
mut r := router.NewRouter()
router.Get(r, "/tags/:tag", show_tag)
router.Get(r, "/blog/:slug", show_post)
mut s := http.NewServer(18579)
http.SetHost(s, "127.0.0.1")
http.Use(s, r)
router.Get(r, "/late", late)
if not http.RunBackground(s):
println "port 18579 is busy"
ret 1
end
fetch("/tags/open%20source")
fetch("/blog/hello-salam?page=2")
fetch("/late")
ret 0
endServing a folder next to the API
Static(r, prefix, folder) serves the files under a folder for paths that start with the prefix, with the Content-Type from net/mime and index.html for a directory. A directory requested without its trailing slash gets a 301 redirect to the slashed path, and .. cannot climb out of the folder. Static files are checked before the routes and for every method, and a path with no file falls through to the routes and then to a 404, so /api/status still reaches its handler. The program writes a small public folder, serves it on 127.0.0.1 and fetches four paths.
import net.http
import net.router
import os
func status(ctx: i64): http.Json(ctx, "{\"ok\":true}") end
func fetch(path: str):
resp := http.Get("http://127.0.0.1:18578" + path)
println path, resp.status, http.GetHeader(resp, "Content-Type")
end
func main:
os.MkdirAll("public/css")
os.WriteFile("public/index.html", "<h1>Hello</h1>")
os.WriteFile("public/css/site.css", "h1 { color: teal }")
mut r := router.NewRouter()
router.Get(r, "/api/status", status)
router.Static(r, "/", "public")
mut s := http.NewServer(18578)
http.SetHost(s, "127.0.0.1")
http.Use(s, r)
if not http.RunBackground(s):
println "port 18578 is busy"
ret 1
end
fetch("/")
fetch("/css/site.css")
fetch("/api/status")
fetch("/missing.png")
os.RemoveAll("public")
ret 0
endThe prefix is a plain string prefix. With / every path is first looked up on disk, which is fine for a small site; for a large API put the files under their own prefix such as /static. A router has one static folder: a second Static call replaces the first. EnableListing makes a folder without index.html answer with an HTML list of its files instead of a 404.
Types
Param
struct Param| Name | Type | Description |
|---|---|---|
key | str = "" | The name, without the :. |
value | str = "" | The value from the request path. |
import net.router
mut count := 0
func list(_ctx: i64):
count += 1
end
func show(_ctx: i64):
count += 1
end
func create(_ctx: i64):
count += 1
end
func main:
mut r := router.NewRouter()
router.Get(r, "/books", list)
router.Get(r, "/books/:id", show)
router.Post(r, "/books", create)
router.Put(r, "/books/:id", create)
router.Delete(r, "/books/:id", create)
router.Patch(r, "/books/:id", create)
router.Head(r, "/books", list)
router.Options(r, "/books", list)
router.Method(r, "PURGE", "/cache", create)
router.Method(r, "GET", "/files/*", show)
router.Group(r, "/api/v1", "GET", "/users/:uid/books/:id", show)
router.Static(r, "/static", "public")
router.EnableListing(r)
println r.routes.len(), r.has_static, r.static_prefix, r.static_listing
mut m := router.Dispatch(r, "GET", "/books/42")
println m.matched, m.fn == show, router.ParamGet(m, "id")
router.FreeMatch(m)
mut m2 := router.Dispatch(r, "GET", "/api/v1/users/7/books/3")
println m2.matched, m2.params.items.len(), m2.params.items[0].key, m2.params.items[0].value
router.FreeMatch(m2)
mut m3 := router.Dispatch(r, "GET", "/files/a/b.txt")
println m3.matched, m3.params.items.len()
router.FreeMatch(m3)
mut m4 := router.Dispatch(r, "DELETE", "/books")
println m4.matched, r.routes[0].method, r.routes[0].pattern
router.FreeMatch(m4)
endParamList
struct ParamList| Name | Type | Description |
|---|---|---|
items | Vector | The parameters. |
The example for this item is shown with the ones above: net-router-Dispatch.salam
Route
struct Route| Name | Type | Description |
|---|---|---|
method | str = "" | The HTTP method. |
pattern | str = "" | The path pattern. |
fn | i64 = 0 | The handler function. |
mid | int = 0 | The method id. |
spec | int = 0 | The kind of pattern: zero literal, 1 with parameters, 2 with a *. |
The example for this item is shown with the ones above: net-router-Dispatch.salam
Router
struct Router| Name | Type | Description |
|---|---|---|
routes | Vector | The registered routes. |
static_prefix | str = "" | The path prefix of the static files. |
static_dir | str = "" | The directory of the static files. |
has_static | bool = false | Static files are enabled. |
static_listing | bool = false | Show the contents of directories. |
fallback | i64 = 0 | The handler that runs when no route matches. Zero means there is none and the server answers 404. |
idx | Vector | The hash table of the literal routes. |
idx_mask | int = 0 | The table size minus one. |
n_literal | int = 0 | The number of literal routes. |
n_dynamic | int = 0 | The number of routes with parameters. |
The example for this item is shown with the ones above: net-router-Dispatch.salam
Match
struct Match| Name | Type | Description |
|---|---|---|
matched | bool = false | A route was found. |
fn | i64 = 0 | The handler function of the route. |
params | ParamList | The path parameters. |
The example for this item is shown with the ones above: net-router-Dispatch.salam
Functions
NewRouter
func NewRouter(): RouterThe example for this item is shown with the ones above: net-router-Dispatch.salam
Get
func Get(r &: Router, path: str, fn: i64)The example for this item is shown with the ones above: net-router-Dispatch.salam
Post
func Post(r &: Router, path: str, fn: i64)The example for this item is shown with the ones above: net-router-Dispatch.salam
Put
func Put(r &: Router, path: str, fn: i64)The example for this item is shown with the ones above: net-router-Dispatch.salam
Delete
func Delete(r &: Router, path: str, fn: i64)The example for this item is shown with the ones above: net-router-Dispatch.salam
Patch
func Patch(r &: Router, path: str, fn: i64)The example for this item is shown with the ones above: net-router-Dispatch.salam
Head
func Head(r &: Router, path: str, fn: i64)The example for this item is shown with the ones above: net-router-Dispatch.salam
Options
func Options(r &: Router, path: str, fn: i64)The example for this item is shown with the ones above: net-router-Dispatch.salam
Method
func Method(r &: Router, method: str, path: str, fn: i64)PROPFIND. The name is matched case sensitively.The example for this item is shown with the ones above: net-router-Dispatch.salam
Group
func Group(r &: Router, prefix: str, method: str, path: str, fn: i64)/api/v1; it is the same as registering prefix + path with that method.The example for this item is shown with the ones above: net-router-Dispatch.salam
Static
func Static(r &: Router, prefix: str, dir: str)The example for this item is shown with the ones above: net-router-Dispatch.salam
EnableListing
func EnableListing(r &: Router)index.html, shows the list of files.The example for this item is shown with the ones above: net-router-Dispatch.salam
SetFallback
func SetFallback(r &: Router, fn: i64)Host header, such as net/httpd, sits behind one router.import net.http
import net.router
func home(ctx: i64):
http.Text(ctx, "home")
end
func anything_else(ctx: i64):
http.SetStatus(ctx, 202)
http.Text(ctx, "fallback got " + http.Method(ctx) + " " + http.Path(ctx))
end
func main:
mut r := router.NewRouter()
println r.fallback
router.Get(r, "/", home)
router.SetFallback(r, anything_else)
println r.fallback == anything_else
mut s := http.NewServer(19623)
http.Use(s, r)
println http.RunBackground(s)
a := http.Get("http://127.0.0.1:19623/")
println http.Status(a), http.Body(a)
b := http.Get("http://127.0.0.1:19623/unknown/page")
println http.Status(b), http.Body(b)
endParamGet
func ParamGet(m: Match, key: str): strThe example for this item is shown with the ones above: net-router-Dispatch.salam
Dispatch
func Dispatch(r: Router, method: str, path: str): MatchThe example for this item is shown with the ones above: net-router-Dispatch.salam
FreeMatch
func FreeMatch(m &: Match)The example for this item is shown with the ones above: net-router-Dispatch.salam