Packages

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

MethodSends
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
main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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 seen
Click the code to edit it

c.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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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.

Quick check

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?

Your turn

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.

main.osl
Ctrl Enter runs
Expected output
200 {"message":"Hello, Ada!"}
400 {"error":"name is required"}
400 {"error":"name is required"}
Quick check

app.get("/users/:id", ...) receives /users/42. What is c.param("id")?

Your turn

A visit counter

Add GET /visit. Each call answers with how many times it has been called so far, as plain text.

main.osl
Ctrl Enter runs
Expected output
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.Handler

basicAuth 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.Handler

cors 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.Handler

corsOpen returns cors middleware allowing any origin, every common method and any header.

#serve.logger() serve.Handler

logger returns middleware that prints one line per request: time, method, path, status, response bytes and duration.

#serve.maxBodySize(int maxBytes) serve.Handler

maxBodySize returns middleware that limits the request body of the routes it guards to maxBytes; larger bodies answer 413.

#serve.new(object? options) *serve.Router

new 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.Handler

noCache returns middleware that tells clients not to cache responses.

#serve.rateLimit(object? options) serve.Handler

rateLimit 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.Handler

Deprecated. 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.Handler

requestID 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.Handler

requireBearer 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.Handler

requireHeader returns middleware that answers 400 unless header name equals value, ignoring case.

#serve.secureHeaders() serve.Handler

secureHeaders returns middleware that sets common security headers (nosniff, frame denial, no referrer).

#serve.setKey(string key, any value) serve.Handler

setKey returns middleware that stores value under key on every request (see c.get).

#serve.timeLimit(int timeout) serve.Handler

timeLimit 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.Handler

Deprecated. 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) boolean

accepts 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() object

Deprecated. 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() string

body 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() string

contentType returns the request's media type without parameters, such as "application/json", or "" when it has none.

#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) string

Deprecated. Deprecated: use c.formValue(key) ?? def instead.

formValueDefault returns a form field, or def when it is absent or empty.

#context.fullURL() string

fullURL returns the absolute URL of the request, from scheme(), the Host header and the request path and query.

#context.get(string key) any

get returns a value stored with set, or null.

#context.getBool(string key) boolean

Deprecated. Deprecated: use c.get(key) with a typed declaration instead.

getBool returns a stored boolean, or false.

#context.getFloat(string key) number

Deprecated. Deprecated: use c.get(key) with a typed declaration instead.

getFloat returns a stored number, or 0.

#context.getInt(string key) int

Deprecated. Deprecated: use c.get(key) with a typed declaration instead.

getInt returns a stored integer, or 0.

#context.getString(string key) string

Deprecated. Deprecated: use c.get(key) with a typed declaration instead.

getString returns a stored string, or "".

#context.hasHeader(string name, string value) boolean

hasHeader 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() string

host 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() string

ip 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() boolean

isAborted reports whether abort was called.

#context.isAjax() boolean

isAjax reports whether the request carries X-Requested-With: XMLHttpRequest.

#context.isForm() boolean

isForm reports whether the request body is a URL-encoded or multipart form.

#context.isJSON() boolean

isJSON reports whether the request body is JSON.

#context.isWebSocket() boolean

isWebSocket reports whether the request asks to upgrade to a WebSocket.

#context.isWritten() boolean

isWritten 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() string

method 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) string

param 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() string

path 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) string

Deprecated. 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() string

remoteAddr 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() int

responseSize returns the number of body bytes written so far.

#context.scheme() string

scheme 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() int

statusCode 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() boolean

Deprecated. 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.Router

bodyLimit 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.Router

group 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.Router

layout 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.Router

locals 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.Router

use adds middleware that runs before the handlers of every route registered after it, in order. Returns the router.

#router.views(string dir) *serve.Router

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