server
HTTP and WebSocket server backed by Rust axum on tokio. Pair it with network for local round-trip tests.
global setup(){
import("server");
import("network");
}
Built from the Rust crate rust/server; no Boost or Crow dependency.
API
Register routes while the server is stopped, then start(port) / stop().
| Function | Description |
|---|---|
routeGet(path, body) | Fixed GET response rendered as text/html; charset=utf-8 |
routePost(path, body) | Fixed POST response rendered as text/html; charset=utf-8 |
get(path, html) | HTML GET route alias for routeGet |
post(path, html) | HTML POST route alias for routePost |
htmlGet(path, html) | Explicit HTML GET route alias |
htmlPost(path, html) | Explicit HTML POST route alias |
routeEcho(path) | POST that returns the request body |
wsEcho(path) | WebSocket echo endpoint |
clearRoutes() | Drop pending routes |
start(port) | Listen on 127.0.0.1:port in the background |
stop() | Stop the listener |
running() | true while listening |
port() | Bound port, or 0 |
Paths must start with /. The listener binds to 127.0.0.1 only.
HTML is supplied as a normal Lynxer string and is returned unchanged. For example:
global main(){
global.server.get("/", "<!doctype html><html><body><h1>Hello</h1></body></html>");
println(global.server.start(8080));
while (global.server.running()) {}
}
Routes
Register routes while the server is stopped. Paths must start with /, and a duplicate of the same verb and path is rejected.
| Function | Description |
|---|---|
routeGet(path, body) / get / htmlGet | Fixed GET response as text/html; charset=utf-8 |
routePost(path, body) / post / htmlPost | Fixed POST response |
put(path, body) / delete(path, body) / patch(path, body) | The other verbs |
anyHttp(path, body) | One route answering GET, POST, PUT, DELETE and PATCH |
routeGetCallback(path, functionName) / routePostCallback(...) | Run a named Lynxer handler for each matching request |
routeCallback(path, functionName) | Run a named handler for GET, POST, PUT, DELETE or PATCH |
respond(status, contentType, body) | Set the current callback request's response |
getStatus(path, body, status) | HTML GET with a custom status code |
routeEcho(path) | POST that returns the request body as text/plain |
jsonGet(path, json) / jsonPost(path, json) | JSON responses (application/json) |
jsonRoute(path, json) | JSON for GET and POST |
jsonStatus(path, json, status) | JSON with a custom status code |
redirect(path, target) / redirect301(path, target) | 302 / 301 redirects |
template(path, file, dataJson) / templatePost(...) | Render a template file |
templateString(path, source, dataJson) | Render an inline template |
serveFile(path, filepath) | Serve one file at path |
staticFiles(urlPrefix, directory) | Serve directory under the URL prefix |
staticSite(directory) | Serve directory; / maps to /index.html |
wsEcho(path) | WebSocket echo endpoint |
clearRoutes() | Drop pending routes, WebSocket paths and static trees |
HEAD is served by the matching GET route.
Callback handlers take no parameters. They run for each request under the interpreter lock; use the request readers below and then call respond. A callback that omits respond returns 204; a callback error returns 500. Fixed-string route registration and responses remain unchanged.
global greetRequest(){
str name = global.server.getArg("name");
global.server.respond(200, "text/plain", "Hello " + name);
}
global setup(){
import("server");
global.server.routeGetCallback("/greet", "greetRequest");
}
Templates
dataJson is a JSON object whose keys become template variables, addressed as {{ key }} and {{ nested.key }}. A Jinja-compatible subset is implemented:
{{ expression }}— output, with filters ({{ name | upper }}){% if %}/{% elif %}/{% else %}/{% endif %}{% for x in items %}/{% endfor %}— withloop.index,loop.index0,loop.first,loop.lastandloop.length;itemsmay be an array, an object (its keys), a string (its characters) orrange(n){% set name = expression %}{# comment #}
Expressions cover literals, dotted paths, or/and/not, ==/!=/</ <=/>/>=, in, +/-/*///%, ~ (concatenation), grouping and range(), plus the filters upper, lower, trim, capitalize, title, length, first, last, reverse, join, default, int, float, round, replace and string. Templates support {% include "file" %}, {% extends "base" %} with named {% block %} sections, and {% macro name(args) %} definitions called as {{ name(args) }}. Output expressions are HTML-escaped by default; {{ value | safe }} opts out for trusted markup. Dynamic values inside on*, style, srcdoc and srcset attributes are omitted, and dangerous URL schemes in URL attributes are replaced with #. Unknown variables return a descriptive 500 template error instead of silently rendering as empty; use default for intentionally optional values. setTemplateFolder(folder) sets the directory used to resolve templates and included/extended files; those paths cannot escape the configured folder.
A template renders inside the request, so it can read the request through the request object:
| Path | Value |
|---|---|
request.method / request.path / request.query | The request line |
request.args | Object of query-string parameters |
request.headers / request.cookies | Objects of header / cookie values |
request.body / request.contentType | Raw body and content type |
request.json | The body parsed as JSON (application/json only) |
request.form | The body parsed as a form (form-urlencoded only) |
The query-string arguments also overlay the root context, so ?name=Ada makes {{ name }} render Ada even without the prefix.
global.server.templateString(
"/greet",
"<h1>Hello, {{ name | upper }}!</h1>"
"{% if request.args.n | default(\"\") %}<p>n={{ request.args.n }}</p>{% endif %}",
"{\"name\":\"World\"}");
Middleware and error bodies
| Function | Description |
|---|---|
cors() | Access-Control-Allow-Origin: * on every response |
corsOrigin(origin) | Allow one origin instead of * |
addGlobalHeader(key, value) | Append a header to every response |
enableRequestLog() | Print METHOD /path for each request |
setDebug(enabled) | Debug flag; also enables request logging |
notFound(body) / serverError(body) / forbidden(body) / methodNotAllowed(body) | Replace the default 404 / 500 / 403 / 405 body |
With CORS enabled, an OPTIONS preflight is answered 204 with Access-Control-Allow-Methods and -Headers.
Request context
Inside callback handlers, these read that handler's current request. Outside a callback they retain compatibility by describing the most recently handled request. Each returns "" when no request has been handled yet.
| Function | Description |
|---|---|
getMethod() / getPath() / getUrl() | Verb, path, and http://host/path?query |
getBody() / getContentType() | Raw body and the request Content-Type |
getHeader(name) / getCookie(name) | A header (case-insensitive) or cookie value |
getArg(name) / getForm(name) | Query-string parameter / form field |
getRemoteAddr() | Client IP address |
Lifecycle
| Function | Description |
|---|---|
start(port) | Listen on 127.0.0.1:port in the background |
stop() | Stop the listener |
running() / port() | Listener state and bound port |
init(host, port) | Record the host and port for run() |
run() | Start on the recorded host/port, then block until the process ends |
runHTTPS(cert, key) | Start a TLS listener on the init host/port from a PEM certificate and key |
runSSLAdhoc() | Start a TLS listener with a self-signed certificate generated in-process |
run() blocks, matching the original, and releases the interpreter lock while waiting so callback routes continue to run. With start() + stop(), a long-lived Lynxer loop should yield periodically (for example with nativeThreadYield(0.01)) to let request callbacks acquire the interpreter.
TLS
runHTTPS(certPath, keyPath) and runSSLAdhoc() start an HTTPS listener with the same routes, on the host and port recorded by init(host, port) — call that first, or the op answers ERROR: call init(host, port) before …. Unlike run() they do not block: they return "ok" once the socket is listening, and stop() shuts the listener down. Both bound the port before returning, so a port clash is reported by the call.
runHTTPS reads the PEM files and rejects one it cannot use before binding: a missing file reports ERROR: cannot read '<path>': …, a file with no certificate reports ERROR: the certificate file contains no PEM certificate, and a key file with no key reports ERROR: the key file contains no PEM private key. A certificate/key mismatch is rejected before binding with ERROR: the certificate and private key do not match.
runSSLAdhoc() mints a self-signed certificate for localhost and 127.0.0.1 with rcgen, so it needs no files at all — it is the quickest way to stand up HTTPS, and a client must skip verification (the certificate is not signed by any authority it trusts).
TLS is rustls with the ring provider behind axum-server — pure Rust, no system OpenSSL — with rustls-pemfile for the PEM files and rcgen for the self-signed certificate. docs/stdlib_server_tls.lynx is the fixture: it mints a certificate, serves over HTTPS and completes a real handshake.
See also
- stdlib-contracts — the contract this module implements, including the error sentinel family it uses.
- builtins — the functions the interpreter implements itself.
- limitations — the full divergence register.