Skip to content

httpserver

The httpserver package runs an HTTP server. Register handlers for routes, add middleware, and start it. Handlers receive a Request and a Response. A server is built on the web interface, so it can be mounted beside other web apps, wrapped in web middleware and tested without a network; see Working with web.

server = httpserver::create(18087)
server.get("/", with (req, res) do
res.text("Hello, World!")
end)
server.get("/greet/:name", with (req, res) do
res.json({message: "Hello, ${req.param("name")}!"})
end)
server.start() # serves on a background task
task::sleep(200)
println http::get("http://localhost:18087/greet/ada").json() # {"message": "Hello, ada!"}
server.stop()

start() returns immediately and serves requests in the background, which suits scripts that do other work. A program whose only job is serving should call server.start(true), which blocks until server.stop() is called.

The server logs each request to stderr; set .logging("none") in the configuration to turn that off. httpserver is unavailable in safe mode.

get, post, put, patch and delete register a handler for a method and path; all_methods matches any method. Each returns the server, so registrations can be chained. Paths can contain:

  • literal segments: /users/profile
  • named parameters: /users/:id, read with req.param("id")
  • a trailing wildcard: /files/*, read with req.param("*")

group(prefix) returns a router whose routes all start with prefix:

api = server.group("/api/v1")
api.get("/users", with (req, res) do
res.json([{id: 1, name: "Alice"}])
end)
api.post("/users", with (req, res) do
res.json({created: req.json()}, 201)
end)

Requests that match no route get a 404 Not Found. A HEAD request is answered by the matching GET route, without the body.

MethodReturns
method(), path()The HTTP method and path
param(name), params()Route parameters
query(), query_param(name), query_all(name)Query string values; when a key repeats, query keeps the last value and query_all returns every one
header(name), headers()Request headers (names are case-insensitive)
body()The raw body as a string
body_bytes()The raw body as bytes, for binary uploads
json(), is_json()The body parsed as JSON
form(), is_form()Form fields from a URL-encoded body
cookie(name), cookies()Request cookies
content_type(), remote_addr()The content type and client address
id()The request id from web::request_id(), if the server is wrapped with it
env()The request’s web environment
MethodSends
text(body, status)Plain text
html(body, status)HTML
json(value, status)JSON
file(path)A file, with a content type based on its extension
redirect(url, status)A redirect
send(status, body)A raw body, as a string or bytes
stream(body, status)A body sent in chunks as it is produced; see Streaming
not_found(), bad_request(), unauthorized(), forbidden(), server_error()An error status with an optional message

header(name, value), cookie(name, value, path, max_age), clear_cookie(name) and no_cache() set response details before sending. Header names are sent lowercase, as HTTP/2 requires and the web interface expects; HTTP treats names case-insensitively, so clients are unaffected unless they compare names exactly. Each response is sent once; check sent() to see whether a handler already responded, and status_code() for the status it sent.

use adds a function that runs before every route handler. It receives the request, the response and nxt; call nxt() to continue to the next middleware or the handler, or send a response and return without calling it to stop the request there.

server.use(with (req, res, nxt) do
started = time::ticks()
nxt()
eprintln "${req.method()} ${req.path()} took ${time::ticksms(time::ticks() - started)}ms"
end)
server.use(with (req, res, nxt) do
if req.header("authorization") != "Bearer secret"
res.unauthorized("Invalid token")
return
end
nxt()
end)
server.static_files("/public", "./public") # serves ./public/* at /public/*
server.get("/", with (req, res) do
res.file("./public/index.html")
end)

httpserver::config() builds a configuration fluently; pass it to create_with_config:

cfg = httpserver::config()
.host("0.0.0.0")
.port(9000)
.workers(8)
.max_body_size(1024 * 1024)
.request_timeout(10000)
.cors("https://myapp.example.com")
.logging("simple") # "combined", "simple" or "none"
server = httpserver::create_with_config(cfg)

The server is built on Kestrel, the HTTP server behind ASP.NET Core, which parses requests and manages connections and keep-alive. A few limits protect it from slow or oversized requests and from overload, and each answers the client with a status code instead of reaching a handler:

LimitDefaultSet withResponse
Handlers running at once64workers(n)503 if no slot frees up within five seconds
Request body size8 MiBmax_body_size(bytes)413
Time for a handler to respond30 secondsrequest_timeout(ms)504; the handler’s late response is discarded
Time for a client to send its headers10 seconds408
Malformed request400

server.stop() answers requests still waiting on a handler with 503.

res.stream(body) sends the headers at once and then each chunk as the body produces it, which suits large downloads and server-sent events. The body is a generator that yields chunks, or a lambda that receives a write(chunk) function. write returns false once the client has gone away:

server = httpserver::create(18088)
server.get("/events", with (req, res) do
res.header("content-type", "text/event-stream")
res.stream(with (write) do
for i in [1 to 3] do
break when !write("data: ${i}\n\n")
task::sleep(100)
end
end)
end)
server.start()
task::sleep(200)
println http::get("http://localhost:18088/events").body.size() # 27
server.stop()

A streaming response keeps its worker until it ends, so raise workers in the configuration for many long-lived streams.

A server is a web app. to_app() returns it with its logging, CORS and wrappers applied, wrap adds web middleware around the whole server, and web::request calls it without starting a listener, which makes handlers quick to test:

server = httpserver::create_with_config(httpserver::config().logging("none"))
server.wrap(web::request_id())
server.get("/users/:id", with (req, res) do
res.json({"id": req.param("id"), "request": req.id().size()})
end)
r = web::request(server.to_app(), "GET", "/users/7")
println r.json() # {"id": "7", "request": 36}
site = web::builder()
.map("/api", server.to_app())
.run(with (env) do web::text_response("home") end)
println web::request(site, "GET", "/api/users/8").json()["id"] # 8
server.on_start(with () do
eprintln "listening"
end)
server.on_stop(with () do
eprintln "shutting down"
end)
server.on_error(with (err, req, res) do
eprintln "error on ${req.path()}: ${err}"
res.json({error: "internal error"}, 500)
end)

Without an error hook, a handler that throws produces a 500 response.

To stop cleanly on Ctrl+C, combine start(true) with a signal handler that calls server.stop().

httpserver::create(port = 8080, host = "localhost")

Creates a new HTTP server with default configuration.

ParameterTypeDefaultDescription
portinteger8080Port to listen on (default 8080).
hoststring"localhost"Bind address (default "localhost").

Returns Server

httpserver::create_with_config(cfg)

Creates a new HTTP server from a Config object.

ParameterTypeDefaultDescription
cfgConfigServer configuration.

Returns Server

httpserver::config()

Returns a new Config builder.

Returns Config

Server configuration. All setter methods return self for fluent chaining.

Config.new()
host(v)

Sets the bind host.

ParameterTypeDefaultDescription
vstringHostname or IP address.

Returns Config — self

port(v)

Sets the listening port.

ParameterTypeDefaultDescription
vintegerPort number.

Returns Config — self

workers(v)

Sets how many requests can run handlers at once (default 64). Further requests wait up to five seconds for a free slot, then get 503 Service Unavailable.

ParameterTypeDefaultDescription
vintegerWorker count.

Returns Config — self

timeout(v)

Sets the accept-poll timeout in milliseconds.

ParameterTypeDefaultDescription
vintegerMilliseconds to wait per accept call.

Returns Config — self

max_body_size(v)

Sets the largest request body accepted, in bytes (default 8 MiB). A larger body gets 413 Content Too Large without reaching a handler.

ParameterTypeDefaultDescription
vintegerMaximum body size in bytes.

Returns Config — self

request_timeout(v)

Sets how long a handler has to respond, in milliseconds (default 30000). Past that, the client gets 504 Gateway Timeout and the handler's late response is discarded.

ParameterTypeDefaultDescription
vintegerMilliseconds.

Returns Config — self

logging(v)

Sets the access log format.

ParameterTypeDefaultDescription
vstring|boolean"combined", "simple", "none", or false to disable.

Returns Config — self

cors(origin = "*")

Enables CORS with an optional allowed origin.

ParameterTypeDefaultDescription
originstring"*"Allowed origin (default "*").

Returns Config — self

get_host()
get_port()
get_workers()
get_timeout()
get_max_body_size()
get_request_timeout()
get_log()
get_cors()
get_cors_origin()

Represents an incoming HTTP request. It wraps a web::Request, adding route parameters.

Request.new(env, params)
ParameterTypeDefaultDescription
envany
paramsany
env()

Returns the request's web environment.

Returns hashmap

method()

Returns the HTTP method (uppercase).

Returns string

path()

Returns the URL path, percent-decoded.

Returns string

query()

Returns the query string parameters as a hashmap. When a key repeats, the last value wins; use query_all for every value.

Returns hashmap

query_param(name, default_val = null)

Returns a single query parameter, or default if absent.

ParameterTypeDefaultDescription
namestringParameter name.
default_valanynullDefault value (default null).

Returns any

query_all(name)

Returns every value of a query parameter, in order.

ParameterTypeDefaultDescription
namestringParameter name.

Returns list

headers()

Returns all request headers as a hashmap (keys lowercased).

Returns hashmap

header(name)

Returns a header value by name (case-insensitive), or null.

ParameterTypeDefaultDescription
namestringHeader name.

Returns string|null

body()

Returns the raw request body string.

Returns string

body_bytes()

Returns the raw request body as bytes, for binary uploads that body() would corrupt by decoding as UTF-8.

Returns bytes

remote_addr()

Returns the client IP address.

Returns string

id()

Returns the request ID set by the web::request_id() middleware (see Server.wrap), or null.

Returns string|null

param(name)

Returns a route parameter by name, or null if not present.

ParameterTypeDefaultDescription
namestringParameter name (from the pattern, e.g. "id" for /:id).

Returns string|null

params()

Returns all route parameters.

Returns hashmap

content_type()

Returns the Content-Type header value.

Returns string|null

is_json()

Returns true if the Content-Type indicates JSON.

Returns boolean

is_form()

Returns true if the Content-Type indicates form-encoded data.

Returns boolean

json()

Parses and returns the request body as a JSON value (cached).

Returns any

form()

Parses the body as URL-encoded form data (cached).

Returns hashmap

cookie(name)

Returns a cookie value by name, or null (cookies parsed lazily).

ParameterTypeDefaultDescription
namestringCookie name.

Returns string|null

cookies()

Returns all cookies as a hashmap (parsed lazily).

Returns hashmap

Collects the response a handler sends. Each response is sent once; the first send (or helper such as text) wins.

Response.new()
sent()

Returns true if the response has already been sent.

Returns boolean

status_code()

Returns the HTTP status code that was (or will be) sent.

Returns integer

header(key, value)

Sets a response header. Names are case-insensitive and sent lowercase. A non-string value is sent as its text, and a list sends one header line per element.

ParameterTypeDefaultDescription
keystringHeader name.
valueanyHeader value.

Returns Response — self

send(status, body)

Sends the response with a status code and body.

ParameterTypeDefaultDescription
statusintegerHTTP status code.
bodystring|bytesResponse body.
stream(body, status = 200)

Streams the response body: chunks reach the client as they are produced, so the response can be long-lived (such as server-sent events). The body is a generator that yields chunks, or a lambda that receives a write(chunk) function; write returns false once the client has gone away.

ParameterTypeDefaultDescription
bodygenerator|lambdaThe chunk producer.
statusinteger200HTTP status code (default 200).
file(fs_path, status = 200)

Sends a file's contents as the response body.

ParameterTypeDefaultDescription
fs_pathstringFilesystem path to the file.
statusinteger200HTTP status code (default 200).
text(body, status = 200)

Sends a plain-text response.

ParameterTypeDefaultDescription
bodystringResponse body.
statusinteger200HTTP status code (default 200).
html(body, status = 200)

Sends an HTML response.

ParameterTypeDefaultDescription
bodystringHTML body.
statusinteger200HTTP status code (default 200).
json(body, status = 200)

Sends a JSON response.

ParameterTypeDefaultDescription
bodyanyValue to serialize (or a pre-serialized string).
statusinteger200HTTP status code (default 200).
redirect(location, status = 302)

Sends a redirect response.

ParameterTypeDefaultDescription
locationstringTarget URL.
statusinteger302HTTP status code (default 302).
cookie(name, value, path = "/", max_age = null, http_only = true, secure = false, same_site = "Lax")

Appends a Set-Cookie header to the response.

ParameterTypeDefaultDescription
namestringCookie name.
valuestringCookie value.
pathstring"/"Cookie path (default "/").
max_ageintegernullMax-Age in seconds (default null = session cookie).
http_onlybooleantrueHttpOnly flag (default true).
securebooleanfalseSecure flag (default false).
same_sitestring"Lax"SameSite policy: "Strict", "Lax", or "None" (default "Lax").

Returns Response — self

clear_cookie(name, path = "/")

Clears a cookie by setting its expiry to the past.

ParameterTypeDefaultDescription
namestringCookie name.
pathstring"/"Cookie path (default "/").

Returns Response — self

no_cache()

Sets Cache-Control headers to prevent caching.

Returns Response — self

not_found(msg = "Not Found")

Sends a 404 Not Found response.

ParameterTypeDefaultDescription
msgstring"Not Found"Response body (default "Not Found").
bad_request(msg = "Bad Request")

Sends a 400 Bad Request response.

ParameterTypeDefaultDescription
msgstring"Bad Request"Response body (default "Bad Request").
unauthorized(msg = "Unauthorized")

Sends a 401 Unauthorized response.

ParameterTypeDefaultDescription
msgstring"Unauthorized"Response body.
forbidden(msg = "Forbidden")

Sends a 403 Forbidden response.

ParameterTypeDefaultDescription
msgstring"Forbidden"Response body.
server_error(msg = "Internal Server Error")

Sends a 500 Internal Server Error response.

ParameterTypeDefaultDescription
msgstring"Internal Server Error"Response body.
finish()

Returns the collected response as [status, headers, body].

Returns list

A route group that delegates to a Server with a path prefix applied.

Router.new(server, prefix)
ParameterTypeDefaultDescription
serverany
prefixany
get(path, handler)
ParameterTypeDefaultDescription
pathany
handlerany
post(path, handler)
ParameterTypeDefaultDescription
pathany
handlerany
put(path, handler)
ParameterTypeDefaultDescription
pathany
handlerany
patch(path, handler)
ParameterTypeDefaultDescription
pathany
handlerany
delete(path, handler)
ParameterTypeDefaultDescription
pathany
handlerany
all_methods(path, handler)
ParameterTypeDefaultDescription
pathany
handlerany
group(sub_prefix)

Creates a nested sub-router under this group's prefix.

ParameterTypeDefaultDescription
sub_prefixstringAdditional prefix for the sub-group.

Returns Router — New router with combined prefix.

An HTTP server with routing, middleware, and static serving.

Server.new(cfg)
ParameterTypeDefaultDescription
cfgany
get(path, handler)

Registers a GET route handler.

ParameterTypeDefaultDescription
pathstringURL pattern (supports :param and * wildcard).
handlerlambdafn(req, res) handler.

Returns Server — self

post(path, handler)

Registers a POST route handler.

ParameterTypeDefaultDescription
pathany
handlerany
put(path, handler)

Registers a PUT route handler.

ParameterTypeDefaultDescription
pathany
handlerany
patch(path, handler)

Registers a PATCH route handler.

ParameterTypeDefaultDescription
pathany
handlerany
delete(path, handler)

Registers a DELETE route handler.

ParameterTypeDefaultDescription
pathany
handlerany
all_methods(path, handler)

Registers a handler for all HTTP methods.

ParameterTypeDefaultDescription
pathany
handlerany
use(mw)

Adds a middleware function to the pipeline.

ParameterTypeDefaultDescription
mwlambdafn(req, res, next) middleware. Call next() to continue.

Returns Server — self

static_files(url_prefix, dir)

Registers middleware to serve static files from a directory.

ParameterTypeDefaultDescription
url_prefixstringURL path prefix (e.g. "/public").
dirstringFilesystem directory to serve from.

Returns Server — self

on_error(hook)

Registers an error handler: fn(err, req, res).

ParameterTypeDefaultDescription
hooklambdaError handler lambda.

Returns Server — self

on_start(hook)

Registers a startup hook: fn() called after server starts listening.

ParameterTypeDefaultDescription
hooklambdaStartup hook lambda.

Returns Server — self

on_stop(hook)

Registers a shutdown hook: fn() called before server stops.

ParameterTypeDefaultDescription
hooklambdaShutdown hook lambda.

Returns Server — self

group(prefix)

Creates a sub-router for grouping routes under a common prefix.

ParameterTypeDefaultDescription
prefixstringURL path prefix.

Returns Router

wrap(middleware)

Adds a web middleware (a function from app to app, such as web::request_id()) around the whole server. Wrappers run before the server's own middleware, in the order added.

ParameterTypeDefaultDescription
middlewarelambdaThe web middleware.

Returns Server — self

to_app()

Returns the server as a web app, with its logging, CORS and wrappers applied, for mounting in a web::Builder or testing with web::request.

Returns lambda — The app.

start(block = false)

Starts the server.

ParameterTypeDefaultDescription
blockbooleanfalseIf true, block the caller (serve in the calling thread).

Returns Server — self

stop()

Stops the server.

Returns Server — self

id()
running()
cfg()
routes()
middlewares()
error_hook()