Standard library · Network

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 /notes must match exactly, byte for byte (so method names and paths are case sensitive);
  • a segment that starts with : is a parameter: /books/:id matches /books/42 with id set to 42;
  • 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/:name also accepts /users/sara/, but the literal route /notes does not accept /notes/.
  • Parameters are not URL-decoded. /tags/open%20source gives open%20source; pass it through http.UrlDecode. The query string is already split off and is read with http.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.

net-router-guide-rest.salam
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")
end
OutputGET /notes -> list_notes POST /notes -> create_note GET /notes/17 -> show_note with id 17 PUT /notes/17 -> update_note with id 17 DELETE /notes/17 -> delete_note with id 17 DELETE /notes -> 404 GET /notes/17/comments -> 404

A 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.

net-router-guide-order.salam
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")
end
OutputGET /users/me -> me 0 params GET /users/sara -> user 1 params GET /users/sara/ -> user 1 params GET /users/me/ -> user 1 params GET /assets/css/site.css -> assets 0 params GET /about -> page 0 params HEAD /users/sara -> user 1 params POST /users/sara -> no route

404 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.

net-router-guide-allow.salam
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")
end
OutputGET /orders -> 200 DELETE /orders -> 405, Allow: GET, POST GET /invoices -> 404

Grouping 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.

net-router-guide-versions.salam
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)
end
OutputGET /api/v1/users GET /api/v2/users GET /api/v2/users/:id true 5 true

Path 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.

net-router-guide-server.salam
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
end
Output/tags/open%20source 200 raw=open%20source decoded=open source /blog/hello-salam?page=2 200 {"post":"hello-salam","page":"2"} /late 404 404 Not Found

Serving 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.

net-router-guide-static.salam
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
end
Output/ 200 text/html; charset=utf-8 /css/site.css 200 text/css /api/status 200 application/json /missing.png 404 text/plain; charset=utf-8

The 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
One path parameter.
NameTypeDescription
keystr = ""The name, without the :.
valuestr = ""The value from the request path.
net-router-Dispatch.salam
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)
end
Output11 true /static true true true 42 true 2 uid 7 true 0 false GET /books

ParamList

struct ParamList
A list of parameters.
NameTypeDescription
itemsVectorThe parameters.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Route

struct Route
One registered route.
NameTypeDescription
methodstr = ""The HTTP method.
patternstr = ""The path pattern.
fni64 = 0The handler function.
midint = 0The method id.
specint = 0The 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
The router: it sends requests to the right function by method and path.
NameTypeDescription
routesVectorThe registered routes.
static_prefixstr = ""The path prefix of the static files.
static_dirstr = ""The directory of the static files.
has_staticbool = falseStatic files are enabled.
static_listingbool = falseShow the contents of directories.
fallbacki64 = 0The handler that runs when no route matches. Zero means there is none and the server answers 404.
idxVectorThe hash table of the literal routes.
idx_maskint = 0The table size minus one.
n_literalint = 0The number of literal routes.
n_dynamicint = 0The number of routes with parameters.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Match

struct Match
The result of finding a route.
NameTypeDescription
matchedbool = falseA route was found.
fni64 = 0The handler function of the route.
paramsParamListThe path parameters.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Functions

NewRouter

func NewRouter(): Router
An empty router.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Get

func Get(r &: Router, path: str, fn: i64)
Registers the handler for GET requests to a path.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Post

func Post(r &: Router, path: str, fn: i64)
For POST.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Put

func Put(r &: Router, path: str, fn: i64)
For PUT.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Delete

func Delete(r &: Router, path: str, fn: i64)
For DELETE.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Patch

func Patch(r &: Router, path: str, fn: i64)
For PATCH.

The example for this item is shown with the ones above: net-router-Dispatch.salam

func Head(r &: Router, path: str, fn: i64)
For HEAD.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Options

func Options(r &: Router, path: str, fn: i64)
For OPTIONS.

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)
Registers the handler for any method you name, such as 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)
Registers one route under a prefix, for example /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)
Serves the files of the given directory under a path prefix. A router has one static folder; calling it again replaces the first.

The example for this item is shown with the ones above: net-router-Dispatch.salam

EnableListing

func EnableListing(r &: Router)
For directories with no 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)
Sets the handler for every request that no route matches, whatever its method or path; the handler builds the whole response. It is how a server that picks the answer from the Host header, such as net/httpd, sits behind one router.
net-router-Fallback.salam
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)
end
Output0 true true 200 home 202 fallback got GET /unknown/page

ParamGet

func ParamGet(m: Match, key: str): str
The value of one path parameter, still URL-encoded; an empty string when there is none.

The example for this item is shown with the ones above: net-router-Dispatch.salam

Dispatch

func Dispatch(r: Router, method: str, path: str): Match
Finds the right route for the method and path of the request. Literal routes win over routes with parameters, and a HEAD request falls back to the GET route when it has none of its own.

The example for this item is shown with the ones above: net-router-Dispatch.salam

FreeMatch

func FreeMatch(m &: Match)
Frees the memory of the result.

The example for this item is shown with the ones above: net-router-Dispatch.salam