# serve

> Build HTTP servers with routes, path parameters, JSON, middleware, cookies, forms, templates and static files.

This is the Markdown version of https://osl-lang.org/packages/serve. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
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](/packages/ws.md) server on the same router. To call a server from OSL, use [requests](/packages/requests.md).

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:

```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()
```

Output:

```text
200 Hello from OSL
```

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:

```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()
```

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 |

```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()
```

Output:

```text
200 application/json; charset=utf-8 {"langs":["osl","go"],"name":"ada"}
200 text/html; charset=utf-8
200 http://127.0.0.1:8761/json
302 /json
204 0
```

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:

```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()
```

Output:

```text
user 42
that's you
file css/app.css
```

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:

```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()
```

Output:

```text
{"page":3,"q":"osl","tags":["a","b"]}
{"page":1,"q":"","tags":[]}
400 {"error":"query parameter \"page\" is \"two\", not an integer"}
```

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:

```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()
```

Output:

```text
201 {"title":"write docs","done":false}
400 {"error":"title is required"}
400
```

`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:

```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
```

Output:

```text
started: yes osl
410
[200,410]
```

`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:

```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()
```

Output:

```text
200 nosniff
401
200
limited 1 200
limited 2 200
limited 3 429
```

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](/packages/env.md) rather than writing them in the source. Tokens are compared in constant time, and an empty token (an unset variable) refuses every request.

> **Warning**
>
> `serve.rateLimit` counts visitors by `c.ip()`. Behind a reverse proxy on another machine, list it in `serve.new({trustProxy: [...]})`, or every visitor looks like the proxy.

## 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:

```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()
```

Output:

```text
200
401
{"user":"ada"}
{"item":7}
400
```

## 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`:

```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()
```

Output:

```text
hello Ada Lovelace
user="Ada Lovelace"; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax
Ada
nobody
```

`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](/packages/template.md):

```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()
```

Output:

```text
text/html; charset=utf-8
<main><h1>Hello &lt;Ada&gt;</h1></main>
```

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:

```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()
```

Output:

```text
200 text/css; charset=utf-8 body { margin: 0 }
404
404
```

## 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:

```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()
```

Output:

```text
pong
true
```

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](/packages/ws.md) server on a route, so one router serves pages and sockets on the same port. `app.ui(path, target)` shows a [ui](/packages/ui.md) interface that updates live. Both go through the same middleware as ordinary routes.

**Quiz.** 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?

- [ ] It runs anyway.
- [x] It never runs: the chain stopped at the middleware.
- [ ] It runs, but with an empty body.

Answer: Calling `c.next()` is what passes the request on.

### Exercise: 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.

Starting code:

```osl
import "std:serve"
import "std:requests"

auto app = serve.new()

// add your route here

app.start("127.0.0.1:8772").unwrap()

auto url = "http://127.0.0.1:8772/greet"
for body in [{name: "Ada"}, {name: ""}, {other: 1}] (
  auto r = requests.post(url, {json: body}).unwrap()
  log r.status r.body.trim()
)

app.stop().unwrap()
```

It should print:

```text
200 {"message":"Hello, Ada!"}
400 {"error":"name is required"}
400 {"error":"name is required"}
```

Hint 1: `c.bodyJSON().unwrap()` gives an object. Read a field with `data["name"]`, and `?? ""` covers a missing one.

Hint 2: `c.badRequest(message)` answers 400 and ends the chain. Put `return` after it.

A solution:

```osl
import "std:serve"
import "std:requests"

auto app = serve.new()

app.post("/greet", def(*serve.Context c) (
  auto data = c.bodyJSON().unwrap()
  string name = data["name"] ?? ""
  if name == "" (
    c.badRequest("name is required")
    return
  )
  c.json(200, {message: "Hello, " ++ name ++ "!"})
))

app.start("127.0.0.1:8852").unwrap()

auto url = "http://127.0.0.1:8852/greet"
for body in [{name: "Ada"}, {name: ""}, {other: 1}] (
  auto r = requests.post(url, {json: body}).unwrap()
  log r.status r.body.trim()
)

app.stop().unwrap()
```

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

- [ ] The number `42`.
- [x] The string `"42"`.
- [ ] `null`.

Answer: Path parameters are strings. `c.paramInt("id", 0)` converts, and returns a result.

### Exercise: A visit counter

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

Starting code:

```osl
import "std:serve"
import "std:requests"

auto app = serve.new()

// add your route here

app.start("127.0.0.1:8773").unwrap()

for _ in 1 to 3 (
  log requests.get("http://127.0.0.1:8773/visit").unwrap().body
)

app.stop().unwrap()
```

It should print:

```text
1
2
3
```

Hint 1: Keep the count in a variable outside the handler: `int visits = 0`.

Hint 2: Inside the handler, `visits = visits + 1` then `c.text(200, "" ++ visits)`.

A solution:

```osl
import "std:serve"
import "std:requests"

auto app = serve.new()
int visits = 0

app.get("/visit", def(*serve.Context c) (
  visits = visits + 1
  c.text(200, "" ++ visits)
))

app.start("127.0.0.1:8853").unwrap()

for _ in 1 to 3 (
  log requests.get("http://127.0.0.1:8853/visit").unwrap().body
)

app.stop().unwrap()
```

## API reference

## API reference

Every member of `std:serve`, generated from the source of OSL.

### 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:

```osl
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.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) 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.

```osl
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.
