Documentation

Everything you need to know, in one page

A guided tour of serverino: endpoints, routing, requests, responses, configuration, WebSockets, HTTPS and deployment. For the exhaustive list of types and members, see the API reference.

Install

You need a D compiler (DMD 2.109 or newer, or a recent LDC) and dub. The fastest way to start is the project template:

dub init your_app -t serverino
cd your_app
dub run

To add serverino to an existing project:

dub add serverino

Your first endpoint

An endpoint is a free function that takes a Request and an Output. mixin ServerinoMain writes main() for you.

import serverino;
mixin ServerinoMain;

void hello(Request request, Output output)
{
   auto name = request.get.read("name", "world");
   output ~= "Hello, " ~ name ~ "!";
}

With a single handler you can leave the attribute out. As soon as there is a second one, every endpoint must be tagged with @endpoint — otherwise serverino would not know which functions to call.

Endpoints run in separate processes. A global variable you set in one request may or may not be there in the next: it belongs to that worker only. Anything that must be shared belongs in a file, a database or a cache.

Routing and priority

@route decides which requests reach an endpoint. With a string it is an exact match; with a delegate it is whatever you can express in D.

@endpoint @route!"/about"
void about(Request r, Output o) { o ~= "About us"; }

@endpoint @route!(r => r.path.startsWith("/api/"))
void api(Request r, Output o) { o.addHeader("content-type", "application/json"); o ~= "{}"; }

Endpoints are tried in descending @priority order (zero by default). The first one that writes something to the output ends the chain — unless it returns Fallthrough.Yes, which means "I am done, but keep going".

@endpoint @priority(100)
auto onlyGet(Request r, Output o)
{
   if (r.method == Request.Method.Get) return Fallthrough.Yes;   // let the others answer

   o.status = 405;
   return Fallthrough.No;                                        // stop here
}

This is how you write authentication, logging or CORS: a high priority endpoint that either answers or steps aside. See the priority example.

The request

MemberWhat you get
request.pathThe requested path, decoded.
request.methodRequest.Method.Get, .Post, .Put, …
request.getQuery string parameters.
request.postFields of an urlencoded body.
request.formMultipart fields and uploaded files.
request.bodyThe raw body and its content type.
request.headerRequest headers, lowercase names.
request.cookieCookies sent by the client.
request.host, request.isSecureHost, and whether the listener is TLS.
request.dump()Everything above, formatted. Handy while developing.

All of those give you the same small interface: read(key, default), readAll(key), has(key) and data for the underlying array of tuples.

if (request.get.has("page"))
   page = request.get.read("page").to!uint;

foreach(name, value; request.header)
   info(name, ": ", value);

An uploaded file is a FormData: filename, contentType and path, the temporary file serverino saved for you.

auto upload = request.form.read("avatar");

if (upload.isFile)
   copy(upload.path, "/var/www/avatars/" ~ upload.filename.baseName);

The response

output ~= something appends to the body; the default content type is text/html.

output.status = 201;
output.addHeader("content-type", "application/json");
output.addHeader("cache-control", "public, max-age=3600");
output.setCookie(Cookie("session", id).path("/").httpOnly.maxAge(1.hours));
output ~= `{"ok": true}`;

Some headers are managed by serverino and cannot be set by hand: content-length, date, server, status and transfer-encoding.

To send a file, use serveFile: it streams the file instead of loading it in memory, and picks the content type from the extension. It returns false if the file is not there — the 404 is up to you.

@endpoint @route!"/manual.pdf"
void manual(Request r, Output o)
{
   if (!o.serveFile("docs/manual.pdf"))
      o.status = 404;
}

Serving a whole directory? serveFile does not check the path for you. Normalise what came from the request and make sure it is still inside your web root before opening it — that is exactly what this website does.

Configuration

A function marked @onServerInit returns the configuration. It runs in the daemon, before anything is listening.

@onServerInit
ServerinoConfig configure()
{
   return ServerinoConfig
      .create()
      .addListener("0.0.0.0", 8080)
      .setMaxWorkers(10)
      .setMaxRequestTime(5.seconds)
      .setMaxRequestSize(1024 * 1024);
}
SettingMeaning
addListener(addr, port)Where to listen. Call it more than once for more listeners.
setMaxWorkers, setMinWorkersThe size of the worker pool. Leave the minimum at zero and let serverino grow and shrink it.
setMaxRequestTimeHow long an endpoint may take before its worker is killed.
setMaxRequestSizeBody size limit, uploads included.
setMaxWorkerLifetime, setMaxWorkerIdlingWhen to recycle a worker.
enableKeepAlive, setHttpTimeoutConnection handling.
setWorkerUser, setWorkerGroupDrop privileges after binding a low port (POSIX).
setLogLevel, enableServerSignatureLogging, and whether to advertise the server.

To read the command line, take it as a parameter — serverino passes it in:

@onServerInit
ServerinoConfig configure(string[] args) { /* getopt(args, ...) */ }

Lifecycle hooks

AttributeRuns
@onServerInitOnce, in the daemon, to build the configuration.
@onDaemonStart / @onDaemonStopIn the daemon, around the server's life.
@onWorkerStart / @onWorkerStopIn every worker: open your database connection here.
@onWorkerExceptionWhen an endpoint throws.
@onWebSocketUpgradeBefore accepting an upgrade: return false to refuse it.
@onWebSocketStart / @onWebSocketStopIn the process dedicated to one WebSocket connection.
@requestScopeMarks state that must be reset between two requests.

WebSockets

Take a WebSocket instead of an Output. The upgrade handler decides whether the connection is accepted at all; after that, your function runs in a process of its own, for as long as the connection lasts.

@onWebSocketUpgrade
bool upgrade(Request request) { return request.path == "/echo"; }

@endpoint @route!"/echo"
void echo(Request request, WebSocket ws)
{
   while(true)
      if (WebSocketMessage msg = ws.receiveMessage())
         ws.send("you said: " ~ msg.asString);
}

If you prefer callbacks to a loop, set ws.onTextMessage, ws.onBinaryMessage or ws.onCloseMessage. You can send strings, basic types and arrays of basic types; ws.send() with no argument flushes what is left in the buffer. Both styles are in the examples: receive loop and callbacks.

Keeping connections under control

An accepted upgrade is a process, and serverino does not cap how many of them may exist: that decision is yours, and @onWebSocketUpgrade is where you make it. Refusing is just returning false — the client gets a 403.

@onWebSocketUpgrade
bool upgrade(Request request)
{
   if (openConnections() >= 24) return false;      // site-wide
   return allow(request.header.read("x-remote-ip"));   // and per visitor
}

Give the connection an end, too: a loop that runs forever keeps its process alive for as long as an abandoned tab stays open. Close it yourself when the time is up.

immutable until = Clock.currTime + 10.minutes;

while(Clock.currTime < until)
{
   // ... receive, send, sleep ...
}

ws.sendClose();

Counting the open connections needs a number that every worker agrees on, and workers share no memory: keep it in a file, in a database, or count the processes themselves. This site does the latter.

Rate limiting

There is no rate limiter in serverino: a high priority endpoint is one, and it is a dozen lines. The only thing to remember is that workers are processes, so the count cannot live in a variable — a counter in memory would only ever see one worker's share of the traffic.

@endpoint @priority(800)
@route!(r => r.path.startsWith("/api/"))
auto rateLimit(Request request, Output output)
{
   if (allow(request.header.read("x-remote-ip")))
      return Fallthrough.Yes;      // out of the way, let the real endpoint answer

   output.status = 429;
   output.addHeader("retry-after", "1");
   return Fallthrough.No;
}

Enable enableRemoteIp() so the daemon adds the x-remote-ip header: unlike x-forwarded-for, the client cannot set it, so it is the one to trust when nothing is proxying in front of you.

For the counter itself, appending a byte to a file per visitor and per time window is enough, and it has a property a read-modify-write does not: appends are atomic, so nothing is lost when several workers count at the same moment.

HTTPS and certificates

HTTPS is a build configuration (POSIX only, it links OpenSSL):

dub build --config=https

or, in your dub.json:

"subConfigurations": { "serverino": "https" }

A certificate belongs to a listener, so plain and encrypted listeners live happily in the same process, and request.isSecure tells them apart:

sc.addListener("0.0.0.0", 80);
sc.addListener("0.0.0.0", 443, Https("cert.pem", "key.pem"));

Certificates are reloaded without restarting anything and without dropping connections, either by calling Daemon.reloadCertificates() or by sending SIGHUP. That is enough to renew a Let's Encrypt certificate from inside the server itself: answer the ACME http-01 challenge on the plain listener, write the new files, reload. The Let's Encrypt example does exactly that.

This website does none of it: it speaks plain http behind a reverse proxy, for the dullest of reasons — it shares a machine with other sites, so something has to hold port 443 and sort out who gets what. On a host of its own it would carry its own certificate, as the example does. Both arrangements are ordinary; the section below is about the second one.

Hot reload

Rebuild your executable, then ask the daemon to restart its workers. Requests being served are finished first, and no connection is dropped.

PlatformHow
POSIXkill -SIGUSR1 <daemon pid>
WindowsDelete the canary file serverino-<pid>-<hash>.canary from the temp directory.

Deploying

A serverino application is one executable. Run it under systemd, give it the ports it needs and you are done:

[Service]
ExecStart=/srv/myapp/myapp
Restart=always
AmbientCapabilities=CAP_NET_BIND_SERVICE

A reverse proxy is optional — with the https configuration serverino terminates TLS itself — but if you already have nginx or caddy, putting the application behind it is ordinary. This site does exactly that.

Behind nginx or caddy

Two things matter. The client address, because everything reaching your endpoints will otherwise come from the proxy and look like a single very busy visitor; and the WebSocket upgrade, which needs saying out loud in nginx.

location / {
	proxy_pass http://127.0.0.1:8080;

	proxy_set_header Host $host;
	proxy_set_header X-Forwarded-For $remote_addr;   # overwrite, do not append
	proxy_set_header X-Forwarded-Proto $scheme;

	# WebSockets, and enough patience for a long lived one
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_read_timeout 900s;
}

Caddy needs less said: reverse_proxy 127.0.0.1:8080 forwards WebSockets and sets the forwarded headers on its own, and header_up X-Forwarded-For {remote_host} makes the address explicit.

Which address to trust. Read the last entry of x-forwarded-for: your proxy appends the address it really saw, and anything before it came from the client, who may write whatever they like there. If you overwrite the header at the proxy, as above, there is only ever one entry. With no proxy, use enableRemoteIp() and read x-remote-ip, which the daemon fills in itself and a client cannot set.

How it works inside

One daemon process owns the listeners and the event loop (epoll on Linux, kqueue on the BSDs and macOS, select elsewhere). It never runs your code.

A communicator pre-parses the request headers, applies the size and time limits, and only when a request is complete does it hand the socket to a worker — a separate process, which receives the file descriptor and runs your endpoint. Routing tables are built at compile time from the attributes on your functions.

Because workers are processes, a crash costs one request: the daemon still owns the client socket, so when a worker dies holding a request it answers 500 Internal Server Error in its place and starts a replacement. Because workers are pre-forked and reused, the cost of that isolation is paid once, not per request. WebSocket connections get a process of their own, so a thousand idle sockets never occupy the HTTP pool.

The full write-up is in ARCHITECTURE.md.