serve
Build HTTP servers with routes, path parameters, JSON, middleware, cookies, forms, templates and static files.
import "std:serve"std:serve runs HTTP servers. You create a router, attach handlers to paths, and start it. Reach for it for web APIs, websites, webhooks and small internal tools. For live two-way messaging, mount a ws server on the same router. To call a server from OSL, use requests.
Every example on this page starts a server on 127.0.0.1, calls it with requests, prints what came back and stops it, so you can run each one as it is.
A first server#
serve.new() makes a router. app.get(path, handler) adds a route, and the handler receives a *serve.Context that holds the request and writes the response. app.start(address) begins listening and returns as soon as it is ready, which lets one program be both server and client:
import "std:serve"
import "std:requests"
auto app = serve.new()
app.get("/hello", def(*serve.Context c) (
c.text(200, "Hello from OSL")
))
app.start("127.0.0.1:8760").unwrap()
auto reply = requests.get("http://127.0.0.1:8760/hello").unwrap()
log reply.status reply.body
app.stop().unwrap()Change the text, or the status to 201, and run it again.
Handlers with a block body are written def(*serve.Context c) ( ... ). The short -> form only works for a handler that returns a value, and handlers don't, so use the block form.
start fails with a NetworkError if the port is taken, so it returns a result. app.stop() closes the server.
Run or start#
app.run(address) serves and blocks until app.stop() is called. That is what a real server's main file ends with:
import "std:serve"
auto app = serve.new()
app.get("/", def(*serve.Context c) (
c.text(200, "up")
))
log "listening on http://localhost:8080"
app.run(":8080").unwrap()Use start when the program has other work to do after the server is up, and run when serving is the program. ":8080" listens on every interface, and "127.0.0.1:8080" only on this machine.
Responses#
A context has a method for each kind of response:
| Method | Sends |
|---|---|
c.text(status, string) | plain text |
c.json(status, value) | a value encoded as JSON |
c.html(status, string) | an HTML string |
c.send(status, contentType, body) | any string or bytes with a content type |
c.redirect(status, url) | a redirect |
c.noContent() | an empty 204 |
c.file(path) | a file from disk |
import "std:serve"
import "std:requests"
auto app = serve.new()
app.get("/json", def(*serve.Context c) (
c.json(200, {name: "ada", langs: ["osl", "go"]})
))
app.get("/page", def(*serve.Context c) (
c.html(200, "<h1>Hi</h1>")
))
app.get("/old", def(*serve.Context c) (
c.redirect(302, "/json")
))
app.get("/empty", def(*serve.Context c) (
c.noContent()
))
app.start("127.0.0.1:8761").unwrap()
auto base = "http://127.0.0.1:8761"
auto a = requests.get(base ++ "/json").unwrap()
log a.status a.headers["Content-Type"] a.body.trim()
auto b = requests.get(base ++ "/page").unwrap()
log b.status b.headers["Content-Type"]
auto c1 = requests.get(base ++ "/old").unwrap()
log c1.status c1.url
auto d = requests.get(base ++ "/old", {redirects: false}).unwrap()
log d.status d.headers["Location"]
auto e = requests.get(base ++ "/empty").unwrap()
log e.status e.body.len
app.stop().unwrap()JSON keys come out sorted. Notice that requests follows the redirect by default, so the second-to-last call ends up at /json. The redirects: false option shows the redirect itself.
Paths and parameters#
A :name segment matches one part of the path, and c.param("name") reads it. A trailing {rest...} matches everything left. Exact paths win over patterns:
import "std:serve"
import "std:requests"
auto app = serve.new()
app.get("/users/:id", def(*serve.Context c) (
c.text(200, "user " ++ c.param("id"))
))
app.get("/users/me", def(*serve.Context c) (
c.text(200, "that's you")
))
app.get("/files/{path...}", def(*serve.Context c) (
c.text(200, "file " ++ c.param("path"))
))
app.start("127.0.0.1:8762").unwrap()
auto base = "http://127.0.0.1:8762"
log requests.get(base ++ "/users/42").unwrap().body
log requests.get(base ++ "/users/me").unwrap().body
log requests.get(base ++ "/files/css/app.css").unwrap().body
app.stop().unwrap()Try /users/me/extra or /files/ (nothing after the slash) and see which route answers.
Path parameters are always strings. c.paramInt("id", 0) returns a result, so a non-number fails cleanly instead of crashing.
Query strings#
c.query("key") returns the value, or null when the parameter is missing, so pair it with ?? for a default. c.queryInt and c.queryBool convert for you:
import "std:serve"
import "std:requests"
auto app = serve.new()
app.get("/search", def(*serve.Context c) (
string q = c.query("q") ?? ""
int page = c.queryInt("page", 1).unwrap()
c.json(200, {q: q, page: page, tags: c.queryArray("tag")})
))
app.start("127.0.0.1:8763").unwrap()
auto base = "http://127.0.0.1:8763"
log requests.get(base ++ "/search?q=osl&page=3&tag=a&tag=b").unwrap().body.trim()
log requests.get(base ++ "/search").unwrap().body.trim()
auto bad = requests.get(base ++ "/search?page=two").unwrap()
log bad.status bad.body.trim()
app.stop().unwrap()The last request shows a convenient rule: when a handler lets a request error such as a bad query integer escape (here through unwrap()), the client gets a 400 with a JSON message, not a 500. Handle the result yourself with catch when you want a different answer.
Reading JSON bodies#
c.bodyJSON() parses the request body as an object and returns a result. c.bindJSON(out) fills a class instance you pass in. Both fail with ParseError on bad input, and a handler that doesn't catch it answers 400:
import "std:serve"
import "std:requests"
class Todo (
string title = ""
boolean done = false
)
auto app = serve.new()
app.post("/todos", def(*serve.Context c) (
Todo todo = Todo()
c.bindJSON(todo).unwrap()
if todo.title == "" (
c.badRequest("title is required")
return
)
c.json(201, todo)
))
app.start("127.0.0.1:8764").unwrap()
auto url = "http://127.0.0.1:8764/todos"
auto ok = requests.post(url, {json: {title: "write docs"}}).unwrap()
log ok.status ok.body.trim()
auto empty = requests.post(url, {json: {done: true}}).unwrap()
log empty.status empty.body.trim()
auto junk = requests.post(url, {body: "not json"}).unwrap()
log junk.status
app.stop().unwrap()c.badRequest(message) is one of a family: unauthorized (401), forbidden (403), notFound (404) and internalError (500). Each ends the handler chain and sends {"error": message}. They don't return from your function, so follow them with return when more code would otherwise run.
Middleware#
Middleware is a handler that runs before the route's own. It calls c.next() to pass the request on, and can do work before and after. Register it with app.use(...) and it applies to every route added after it:
import "std:serve"
import "std:requests"
auto app = serve.new()
int[] seen = []
app.use(def(*serve.Context c) (
c.setHeader("X-Powered-By", "osl")
c.set("started", "yes")
c.next()
seen.append(c.statusCode())
))
app.get("/ok", def(*serve.Context c) (
c.text(200, "started: " ++ c.get("started"))
))
app.get("/gone", def(*serve.Context c) (
c.text(410, "gone")
))
app.start("127.0.0.1:8765").unwrap()
auto r = requests.get("http://127.0.0.1:8765/ok").unwrap()
log r.body r.headers["X-Powered-By"]
log requests.get("http://127.0.0.1:8765/gone").unwrap().status
app.stop().unwrap()
log seenc.set(key, value) and c.get(key) pass values from middleware to later handlers, such as the logged-in user. A middleware that calls c.abort(...) or one of the error methods (unauthorized, forbidden) and doesn't call next stops the request there.
Built-in middleware#
The package ships the middleware most servers need: logger, cors, corsOpen, requireBearer, basicAuth, rateLimit, maxBodySize, timeLimit, requestID, secureHeaders and noCache. Each is a function that returns a handler, so you call it when you register it:
import "std:serve"
import "std:requests"
auto app = serve.new()
app.use(serve.secureHeaders())
app.get("/public", def(*serve.Context c) (
c.text(200, "anyone")
))
app.get("/admin", serve.requireBearer("s3cret"), def(*serve.Context c) (
c.text(200, "welcome")
))
app.get("/limited", serve.rateLimit({requests: 2, window: 60000}), def(*serve.Context c) (
c.text(200, "ok")
))
app.start("127.0.0.1:8766").unwrap()
auto base = "http://127.0.0.1:8766"
auto a = requests.get(base ++ "/public").unwrap()
log a.status a.headers["X-Content-Type-Options"]
log requests.get(base ++ "/admin").unwrap().status
auto token = {headers: {Authorization: "Bearer s3cret"}}
log requests.get(base ++ "/admin", token).unwrap().status
for i in 1 to 3 (
log "limited" i requests.get(base ++ "/limited").unwrap().status
)
app.stop().unwrap()A route takes any number of handlers, and they run in order, so serve.requireBearer(...) before the real handler guards that one route. Read secrets from env rather than writing them in the source. Tokens are compared in constant time, and an empty token (an unset variable) refuses every request.
Groups#
app.group(prefix) returns a router whose routes all start with a prefix and share the parent's middleware. Middleware added to the group stays in the group, which is how you guard a whole section:
import "std:serve"
import "std:requests"
auto app = serve.new()
app.get("/health", def(*serve.Context c) (
c.text(200, "ok")
))
auto api = app.group("/api")
api.use(serve.requireBearer("s3cret"))
api.get("/me", def(*serve.Context c) (
c.json(200, {user: "ada"})
))
api.get("/items/:id", def(*serve.Context c) (
c.json(200, {item: c.paramInt("id", 0).unwrap()})
))
app.start("127.0.0.1:8767").unwrap()
auto base = "http://127.0.0.1:8767"
log requests.get(base ++ "/health").unwrap().status
log requests.get(base ++ "/api/me").unwrap().status
auto token = {headers: {Authorization: "Bearer s3cret"}}
log requests.get(base ++ "/api/me", token).unwrap().body.trim()
log requests.get(base ++ "/api/items/7", token).unwrap().body.trim()
log requests.get(base ++ "/api/items/x", token).unwrap().status
app.stop().unwrap()Cookies, headers and forms#
Request headers come from c.header(name) (or null), and responses are shaped with c.setHeader. Cookies work the same way. c.setCookie is safe by default: httpOnly, secure and sameSite: "lax" are all on, so turn off only what you must. Forms posted by a browser are read with c.formValue:
import "std:serve"
import "std:requests"
auto app = serve.new()
app.post("/login", def(*serve.Context c) (
string name = c.formValue("name") ?? "stranger"
c.setCookie("user", name, {maxAge: 3600000})
c.text(200, "hello " ++ name)
))
app.get("/whoami", def(*serve.Context c) (
c.text(200, c.cookie("user") ?? "nobody")
))
app.start("127.0.0.1:8768").unwrap()
auto base = "http://127.0.0.1:8768"
auto form = {"Content-Type": "application/x-www-form-urlencoded"}
auto login = requests.post(base ++ "/login", {body: "name=Ada+Lovelace", headers: form}).unwrap()
log login.body
log login.headers["Set-Cookie"]
auto who = requests.get(base ++ "/whoami", {headers: {Cookie: "user=Ada"}}).unwrap()
log who.body
log requests.get(base ++ "/whoami").unwrap().body
app.stop().unwrap()requests doesn't keep cookies between calls, so the example sends the Cookie header itself. A browser would do that for you.
Templates#
app.views(dir) names a folder of .html views and c.render(name, data) fills one in, escaping every value. app.layout(name) wraps each page in a shared layout, which receives the page as body. Views use the syntax of template:
import "std:serve"
import "std:requests"
import "std:fs"
fs.remove("site", {force: true}).unwrap()
fs.mkdir("site/views", {recursive: true}).unwrap()
fs.writeFile("site/views/layout.html", "<main>{{& body}}</main>").unwrap()
fs.writeFile("site/views/hello.html", "<h1>Hello {{ name }}</h1>").unwrap()
auto app = serve.new()
app.views("site/views").layout("layout")
app.get("/hello/:name", def(*serve.Context c) (
c.render("hello", {name: c.param("name")})
))
app.start("127.0.0.1:8769").unwrap()
auto r = requests.get("http://127.0.0.1:8769/hello/%3CAda%3E").unwrap()
log r.headers["Content-Type"]
log r.body
app.stop().unwrap()
fs.remove("site", {force: true}).unwrap()The name arrived as <Ada> and came out escaped, which is what stops user input becoming markup. Write {{& value}} only for HTML you built yourself, as the layout does for body.
Static files#
app.static(prefix, dir) serves a directory. Paths that escape it, dotfiles and directories without an index.html answer 404 unless you opt in with the dotfiles and listing options. app.staticFile(path, file) serves one file at one path:
import "std:serve"
import "std:requests"
import "std:fs"
fs.remove("public", {force: true}).unwrap()
fs.mkdir("public").unwrap()
fs.writeFile("public/app.css", "body { margin: 0 }").unwrap()
fs.writeFile("public/.secret", "hidden").unwrap()
auto app = serve.new()
app.static("/assets", "public")
app.start("127.0.0.1:8770").unwrap()
auto base = "http://127.0.0.1:8770/assets"
auto css = requests.get(base ++ "/app.css").unwrap()
log css.status css.headers["Content-Type"] css.body
log requests.get(base ++ "/missing.css").unwrap().status
log requests.get(base ++ "/.secret").unwrap().status
app.stop().unwrap()
fs.remove("public", {force: true}).unwrap()Serving in the background#
run blocks, so to serve and do something else in one program start the server inside an async function and keep the thread. This is how a program can run an API and a worker side by side:
import "std:serve"
import "std:requests"
auto app = serve.new()
app.get("/ping", def(*serve.Context c) (
c.text(200, "pong")
))
async serveApp() result<unit, error> (
return app.run("127.0.0.1:8771")
)
auto server = serveApp()
thread.sleep(300)
log requests.get("http://127.0.0.1:8771/ping").unwrap().body
app.stop().unwrap()
log server.wait().isOk()The sleep gives the thread time to start listening. When you don't need the thread, app.start(...) does the same with no waiting.
WebSockets and live pages#
app.ws(path, server) mounts a ws server on a route, so one router serves pages and sockets on the same port. app.ui(path, target) shows a ui interface that updates live. Both go through the same middleware as ordinary routes.
A middleware handler checks a token and finds it invalid. It calls c.unauthorized("no") and does not call c.next(). What happens to the route's own handler?
A greeting endpoint
Add POST /greet. It takes JSON like {"name": "Ada"} and answers 200 with {"message": "Hello, Ada!"}. When name is missing or empty it answers 400 with a message of your choice. The program already calls it three times.
200 {"message":"Hello, Ada!"}
400 {"error":"name is required"}
400 {"error":"name is required"}app.get("/users/:id", ...) receives /users/42. What is c.param("id")?
A visit counter
Add GET /visit. Each call answers with how many times it has been called so far, as plain text.
1 2 3
API reference#
120 members, generated from the std:serve source of OSL 9.3.0.
Functions
serve.basicAuth(string username, string password) serve.HandlerbasicAuth returns middleware that answers 401 with a Basic challenge unless the request carries the given username and password, which are compared in constant time. An empty password, such as an unset environment variable, refuses every request, and a warning says so when the middleware is made.
serve.cors(string allowOrigin, string allowMethods, string allowHeaders) serve.Handlercors returns middleware that sets the CORS headers allowing allowOrigin to call with allowMethods and allowHeaders (comma-separated lists), with credentials unless the origin is "*", and answers preflight OPTIONS requests with 204.
serve.corsOpen() serve.HandlercorsOpen returns cors middleware allowing any origin, every common method and any header.
serve.logger() serve.Handlerlogger returns middleware that prints one line per request: time, method, path, status, response bytes and duration.
serve.maxBodySize(int maxBytes) serve.HandlermaxBodySize returns middleware that limits the request body of the routes it guards to maxBytes; larger bodies answer 413.
serve.new(object? options) *serve.Routernew creates a router. Add routes with app.get(path, handler) and the other method functions, then serve it with app.run(address). Option: trustProxy, the reverse proxies in front of the server as IP addresses or CIDR ranges, such as ["10.0.0.0/8"]. Only a request whose direct peer is one of them has its X-Forwarded-For and X-Forwarded-Proto headers read by c.ip(), c.scheme() and c.fullURL(), and so by serve.rateLimit; any client can send those headers, so from anyone else they are ignored.
The default is this machine (127.0.0.0/8 and ::1), so a Cloudflare Tunnel or a local nginx works with no options: cloudflared connects from loopback and passes the visitor's IP in X-Forwarded-For. A proxy on another host (a separate container, a load balancer) must be listed; giving trustProxy replaces the default, so list loopback too if a local proxy is still in use, and pass [] to trust no proxy at all. X-Real-IP and CF-Connecting-IP are never read: configure the proxy to append to X-Forwarded-For (nginx: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for). Throws ValueError for an entry that isn't an IP address or CIDR range.
serve.noCache() serve.HandlernoCache returns middleware that tells clients not to cache responses.
serve.rateLimit(object? options) serve.HandlerrateLimit returns middleware that lets each visitor make at most requests requests per window milliseconds, and answers 429 Too Many Requests after that until the window resets. Both options are required, so a route says exactly what it allows:
app.get("/api", serve.rateLimit({requests: 60, window: date.minute}), handler)A visitor is a client IP, c.ip(): a Cloudflare Tunnel or proxy on this machine works as is, and one on another host must be listed in serve.new's trustProxy. Throws ValueError when an option is missing, requests is less than 1 or window isn't more than 0.
serve.recover() serve.HandlerDeprecated. Deprecated: handlers that throw already answer 500 and have their error printed; drop this middleware.
recover returns middleware that turns a throwing handler into a 500 response carrying the error text.
serve.requestID() serve.HandlerrequestID returns middleware that gives every request an id, reusing the client's X-Request-ID when present, and sets it as the X-Request-ID response header and as c.get("requestID").
serve.requireBearer(string token) serve.HandlerrequireBearer returns middleware that answers 401 unless the request has "Authorization: Bearer <token>". The token is compared in constant time. An empty token, such as an unset environment variable, refuses every request, and a warning says so when the middleware is made.
serve.requireHeader(string name, string value) serve.HandlerrequireHeader returns middleware that answers 400 unless header name equals value, ignoring case.
serve.secureHeaders() serve.HandlersecureHeaders returns middleware that sets common security headers (nosniff, frame denial, no referrer).
serve.setKey(string key, any value) serve.HandlersetKey returns middleware that stores value under key on every request (see c.get).
serve.timeLimit(int timeout) serve.HandlertimeLimit returns middleware that answers 503 when the rest of the chain takes longer than timeout milliseconds; a late response is discarded.
serve.timeout(int seconds) serve.HandlerDeprecated. Deprecated: use serve.timeLimit(milliseconds) instead.
timeout answers 503 when the chain takes longer than seconds.
*serve.Handler
A handle with no methods of its own: pass it to the package's functions.
*serve.Context
context.abort(any ...values)abort stops the handler chain after the current handler. abort(status) also sends an empty response with that status, and abort(status, message) sends {"error": message} as JSON. Nothing is sent if a response was already written.
context.accepts(string mediaType) booleanaccepts reports whether the Accept header allows mediaType; true when the header is missing or accepts /.
context.addHeader(string name, string value)addHeader adds a response header value, keeping earlier ones.
context.attachment(string path, string filename)attachment sends the file at path as a download named filename.
context.badRequest(string message)badRequest aborts with 400 and {"error": message}.
context.basicAuth() objectDeprecated. Deprecated: use c.credentials() instead, which returns null when there are none.
basicAuth returns {ok, username, password} for HTTP Basic credentials.
context.bearer() string?bearer returns the token of an "Authorization: Bearer <token>" header, or null when there is none.
context.bindJSON(any out) result<unit, error>bindJSON decodes the JSON request body into out, an object (whose keys are set from the body) or a class instance. Fails with ParseError when the body isn't valid JSON for out; left uncaught, that answers the client 400.
context.body() stringbody returns the request body as a string; it can be read any number of times. When the body is larger than the router's bodyLimit or can't be read, the client gets 413 or 400 and body throws IOError, which ends the handler without a 500.
context.bodyBytes() byte[]bodyBytes returns the request body as byte[]; it fails like body.
context.bodyJSON() result<object, error>bodyJSON parses the request body as a JSON object. Fails with ParseError when it isn't valid JSON or isn't an object, and like body when it can't be read; left uncaught, a ParseError answers the client 400.
context.bodyJSONArray() result<array, error>bodyJSONArray parses the request body as a JSON array and fails like bodyJSON.
context.clearCookie(string name)clearCookie tells the client to delete a cookie set with path "/".
context.contentType() stringcontentType returns the request's media type without parameters, such as "application/json", or "" when it has none.
context.cookie(string name) string?cookie returns a request cookie's value, or null when it wasn't sent.
context.cookies() string[string]cookies returns every request cookie by name.
context.created(any obj)Deprecated. Deprecated: use c.json(201, value) instead.
created sends value as JSON with status 201.
context.credentials() {password: string, username: string}?credentials returns the HTTP Basic {username, password} the client sent, or null when the request has no Basic Authorization header.
context.data(int code, string contentType, byte[] body)Deprecated. Deprecated: use c.send(status, contentType, body) instead, which takes a string or byte[].
data sends bytes with a content type.
context.file(string path)file sends the file at path, with its Content-Type, range and caching support; a missing file answers 404.
context.flush()flush sends what has been written so far to the client, for streaming responses.
context.forbidden(string message)forbidden aborts with 403 and {"error": message}.
context.formFile(string key) result<{contentType: string, data: byte[], filename: string, size: int}, error>formFile reads an uploaded file from a multipart form and returns {filename, size, contentType, data}, with size in bytes and data as byte[]. Fails with NotFoundError when the form has no such file and IOError when the body is too large (the client then gets 413) or malformed.
context.formValue(string key) string?formValue returns a field of a URL-encoded or multipart form body, or of the query string, or null when it is absent.
context.formValueDefault(string key, string def) stringDeprecated. Deprecated: use c.formValue(key) ?? def instead.
formValueDefault returns a form field, or def when it is absent or empty.
context.fullURL() stringfullURL returns the absolute URL of the request, from scheme(), the Host header and the request path and query.
context.get(string key) anyget returns a value stored with set, or null.
context.getBool(string key) booleanDeprecated. Deprecated: use c.get(key) with a typed declaration instead.
getBool returns a stored boolean, or false.
context.getFloat(string key) numberDeprecated. Deprecated: use c.get(key) with a typed declaration instead.
getFloat returns a stored number, or 0.
context.getInt(string key) intDeprecated. Deprecated: use c.get(key) with a typed declaration instead.
getInt returns a stored integer, or 0.
context.getString(string key) stringDeprecated. Deprecated: use c.get(key) with a typed declaration instead.
getString returns a stored string, or "".
context.hasHeader(string name, string value) booleanhasHeader reports whether header name equals value, ignoring case.
context.header(string name) string?header returns a request header (case-insensitive name), or null when it wasn't sent. Repeated headers give their first value.
context.headers() string[string]headers returns the request headers by canonical name; repeated headers are joined with ", ".
context.host() stringhost returns the Host the request was sent to.
context.html(int status, string html, any ...data)html sends html as a text/html response with the given status. Passing data runs the template named html from the deprecated loadHTMLGlob instead; use render for templates. Throws StateError when data is given and no templates are loaded, and RuntimeError when the template fails.
context.internalError(string message)internalError aborts with 500 and {"error": message}.
context.ip() stringip returns the client's IP address: the address of the connection's peer. Only when that peer is a trusted proxy (see serve.new's trustProxy) is it the right-most X-Forwarded-For address that isn't a trusted proxy. Any client can send that header, so from anyone else it is ignored.
context.isAborted() booleanisAborted reports whether abort was called.
context.isAjax() booleanisAjax reports whether the request carries X-Requested-With: XMLHttpRequest.
context.isForm() booleanisForm reports whether the request body is a URL-encoded or multipart form.
context.isJSON() booleanisJSON reports whether the request body is JSON.
context.isWebSocket() booleanisWebSocket reports whether the request asks to upgrade to a WebSocket.
context.isWritten() booleanisWritten reports whether a response status has been sent.
context.json(int status, any value)json sends value encoded as JSON with the given status. Throws TypeError when value can't be encoded (a function, say).
context.method() stringmethod returns the request method, such as "GET".
context.next()next runs the rest of the handler chain; middleware calls it to pass the request on and can act after it returns.
context.noContent()noContent sends an empty 204 response.
context.notFound(string message)notFound aborts with 404 and {"error": message}.
context.ok(any obj)Deprecated. Deprecated: use c.json(200, value) instead.
ok sends value as JSON with status 200.
context.param(string key) stringparam returns a path parameter of the matched route: for "/users/:id", param("id") is "42" on /users/42. Throws ValueError when the route has no such parameter.
context.paramInt(string key, int def) result<int, error>paramInt returns a path parameter as an integer, or def when it is empty. Fails with ValueError when it isn't an integer; left uncaught, that answers the client 400 instead of 500.
context.path() stringpath returns the request path, such as "/users/42".
context.query(string key) string?query returns the first value of a query parameter, or null when the request has none.
context.queryAll() string[string]queryAll returns every query parameter with its first value.
context.queryArray(string key) string[]queryArray returns every value of a repeated query parameter, such as ["1", "2"] for ?x=1&x=2; empty when absent.
context.queryBool(string key, boolean def) result<boolean, error>queryBool returns a query parameter as a boolean ("true", "false", "1", "0", ...), or def when it is absent or empty. Fails with ValueError when it isn't a boolean; left uncaught, that answers the client 400.
context.queryDefault(string key, string def) stringDeprecated. Deprecated: use c.query(key) ?? def instead.
queryDefault returns a query parameter, or def when it is absent or empty.
context.queryInt(string key, int def) result<int, error>queryInt returns a query parameter as an integer, or def when it is absent or empty. Fails with ValueError when it isn't an integer; left uncaught, that answers the client 400 instead of 500.
context.redirect(int status, string url)redirect sends a redirect to url with the given status (301, 302, 303, 307 or 308).
context.redirectPermanent(string url)Deprecated. Deprecated: use c.redirect(301, url) instead.
redirectPermanent redirects with status 301.
context.referer() string?referer returns the Referer header, or null when it wasn't sent.
context.remoteAddr() stringremoteAddr returns the client's address as host:port.
context.render(string name, object data, object? options)render renders the view views/<name>.html (see app.views) with std:template syntax, HTML-escaping values, merges in the router's locals, wraps the result in the layout view if one is set (it receives the page as body) and sends it as text/html. Options: status (default 200). Throws NotFoundError when a view file is missing and ParseError when a view is malformed.
context.responseSize() intresponseSize returns the number of body bytes written so far.
context.scheme() stringscheme returns "https" or "http": "https" for a TLS connection, or the X-Forwarded-Proto a trusted proxy (see serve.new's trustProxy) set.
context.send(int status, string contentType, any body)send sends body, a string or byte[], with the given status and Content-Type. Throws TypeError for other body types.
context.set(string key, any value)set stores a value on the request for later handlers, such as the user an auth middleware found.
context.setCookie(string name, string value, object? options)setCookie sets a response cookie. Options: maxAge (milliseconds; 0, the default, makes a session cookie), path (default "/"), domain, httpOnly (default true, so page scripts can't read it), secure (default true, so browsers send it only over HTTPS; they also keep a Secure cookie from plain http://localhost, so local development works too) and sameSite ("lax", the default, "strict" or "none"). Throws ValueError for another sameSite, and for sameSite "none" without secure, which browsers reject.
context.setHeader(string name, string value)setHeader sets a response header, replacing earlier values.
context.status(int code)status sets the response status code, such as 201, without a body.
context.statusCode() intstatusCode returns the response status sent so far, or 200 before any.
context.string(int code, string format, any ...values)Deprecated. Deprecated: use c.text(status, body) instead, with a string template for formatting.
string sends a text response, formatting format with values like Go's fmt.Sprintf when values are given.
context.text(int status, string body)text sends body as a text/plain response with the given status.
context.unauthorized(string message)unauthorized aborts with 401 and {"error": message}.
context.upgrade(*ws.Server server) result<unit, error>upgrade hands the request to a std:ws server, which takes over the connection. Fails with ValueError, after answering 400, when the request isn't a WebSocket upgrade. Throws StateError when a response was already written.
context.userAgent() string?userAgent returns the User-Agent header, or null when it wasn't sent.
context.written() booleanDeprecated. Deprecated: use c.isWritten() instead.
written reports whether a response status has been sent.
*serve.Router
router.any(string pattern, any ...handlers)any routes every common method (GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD) for pattern to handlers; see get.
router.bodyLimit(int maxBytes) *serve.RouterbodyLimit sets the largest request body, in bytes, a handler can read (default 32 MiB); larger bodies answer 413. 0 removes the limit. Returns the router.
router.delete(string pattern, any ...handlers)delete routes DELETE requests for pattern; see get.
router.get(string pattern, any ...handlers)get routes GET requests for pattern to handlers, which run in order; each takes a *serve.Context and middleware calls c.next() to continue. ":name" segments match one path segment (read with c.param("name")) and a final "{name...}" matches the rest of the path. A handler that throws answers 500 and has its error printed; the server keeps running. Registering the same method and pattern again replaces the handlers. Throws ValueError without handlers and TypeError for a non-function.
router.group(string prefix, any ...setup) *serve.Routergroup returns a router whose routes all start with prefix and share this router's middleware; middleware added to the group stays in the group. The group renders with this router's views, layout and locals unless it sets its own. If setup is given it is called with the group, so routes can be declared in a block.
router.head(string pattern, any ...handlers)head routes HEAD requests for pattern; see get.
router.layout(string name) *serve.Routerlayout sets the view every c.render wraps its page in; the layout receives the rendered page as body (write {{& body}}). Returns the router.
router.loadHTMLGlob(string pattern) result<unit, error>Deprecated. Deprecated: use app.views(dir) and c.render(name, data), which use std:template syntax.
loadHTMLGlob parses the Go html/template files matching pattern for c.html(status, name, data).
router.locals(object values) *serve.Routerlocals sets values every render merges into its data. Returns the router.
router.options(string pattern, any ...handlers)options routes OPTIONS requests for pattern; see get. Without one, an OPTIONS request runs the middleware and answers 204 with an Allow header.
router.patch(string pattern, any ...handlers)patch routes PATCH requests for pattern; see get.
router.post(string pattern, any ...handlers)post routes POST requests for pattern; see get.
router.put(string pattern, any ...handlers)put routes PUT requests for pattern; see get.
router.run(string address) result<unit, error>run serves the router on address, such as ":8080" or "127.0.0.1:3000", and blocks until stop() is called; run it in a thread to keep going. Fails with NetworkError when the address can't be bound. Throws StateError when the router is already running.
router.runTLS(string address, string certFile, string keyFile) result<unit, error>runTLS is run over HTTPS with the certificate and key in the given PEM files. Fails with NotFoundError or IOError when they can't be read.
router.serve(string addr) result<unit, error>Deprecated. Deprecated: use app.run(address) instead.
serve serves the router on addr until stopped.
router.serveTLS(string addr, string certFile, string keyFile) result<unit, error>Deprecated. Deprecated: use app.runTLS(address, certFile, keyFile) instead.
serveTLS serves the router over HTTPS until stopped.
router.setFuncMap(object funcs)Deprecated. Deprecated: use app.views(dir) and c.render(name, data) instead.
setFuncMap sets the functions Go html/templates can call.
router.start(string address) result<unit, error>start serves the router on address like run, but returns as soon as it is listening, so the program goes on, for example to open a window. The server stops with stop(), or when the program ends. Fails with NetworkError when the address can't be bound. Throws StateError when the router is already running.
router.static(string prefix, string dir, object? options)static serves the files under dir at prefix, so prefix/a.css serves dir/a.css and prefix/docs/ serves dir/docs/index.html. Paths that escape dir, including through ".." or symlinks, answer 404. So, unless opted in, do dotfiles (any path segment starting with ".", such as .env or .git/config) and directories without an index.html. Options: dotfiles (default false) serves dotfiles, and listing (default false) lists the files of a directory that has no index.html.
router.staticFile(string pattern, string path)staticFile serves the single file at path for pattern.
router.stop() result<unit, error>stop closes the server and its mounted WebSocket servers' connections; run() then returns. Stopping a router that isn't running does nothing.
router.ui(string pattern, any target, object? options)UI shows a std:ui interface at pattern, as a web page that updates live over a WebSocket and still works without JavaScript. target's type decides how it behaves: a function returning ui.node is a view, rendered fresh on every update; a function returning an object with view() is a factory, called once per visitor to make that visitor's state, and may take the request's *serve.Context; an object with view() is shared by everyone it is given to. After any action every page updates, sending only what changed. Options: title, the page's title ("OSL" by default); maxConnections, how many pages may be live at once (1000), and maxConnectionsPerAddress, how many from one address as c.ip() sees it, an IPv6 address counting by its /64 (100), with at most 16 in one visitor's session, past which a page still works by posting its form; and messagesPerSecond, how many messages a live page may send a second (20, with bursts of twice that), past which they are dropped.
The compiler checks target's type, so a function whose result has no type is an error that asks for its return type.
app.UI("/", def(*serve.Context c) -> TodoList(user = c.get("user")))router.use(any ...handlers) *serve.Routeruse adds middleware that runs before the handlers of every route registered after it, in order. Returns the router.
router.views(string dir) *serve.Routerviews sets the directory c.render reads <name>.html views from. Returns the router.
router.ws(string pattern, *ws.Server server)ws mounts a std:ws server (from ws.new()) on pattern: WebSocket upgrade requests go to it after the middleware, and other requests reach any regular route on the same pattern. stop() also stops the server's connections.