Mesh

Web ​

Mesh includes HTTP and WebSocket servers and scheduler-aware outbound clients, so you can build web applications without external packages. The uppercase HTTP module is the inbound server surface; lowercase Http is the outbound client.

Autonomous clusters: This page explains web primitives. Continue with Autonomous Clusters for adaptive ingress routing and admission control.

HTTP Server ​

Create an HTTP server by building a router, adding routes, and starting the server with HTTP.serve:

mesh
fn handler(request) do
  HTTP.response(200, "Hello from Mesh!")
end

fn main() do
  let r = HTTP.router()
  let r = HTTP.route(r, "/", handler)
  HTTP.serve(r, 8080)
end

The server listens on the specified port and dispatches incoming requests to the matching handler function. Each handler receives a Request and returns a Response.

Creating Responses ​

Use HTTP.response to create a response with a status code and body. Responses default to application/json; charset=utf-8, even when the body is plain text. Use HTTP.response_with_headers to add headers or override Content-Type:

mesh
fn handler(request) do
  let headers = Map.new()
    |> Map.put("Cache-Control", "no-store")
    |> Map.put("Content-Type", "application/json; charset=utf-8")
  HTTP.response_with_headers(200, json { status: "ok" }, headers)
end

For encrypted envelopes and other binary payloads, use Request.body_bytes(request) and HTTP.response_bytes(status, bytes). Binary responses default to application/octet-stream; use HTTP.response_bytes_with_headers(status, bytes, headers) when the response also needs headers such as Cache-Control: no-store.

Common status codes: 200 (OK), 201 (Created), 400 (Bad Request), 401 (Unauthorized), 404 (Not Found), 500 (Internal Server Error).

Routing ​

Basic Routes ​

Use HTTP.route to register a handler for a path. The router checks exact paths first, parameterized paths second, and wildcards last. Within each category, the first registered match wins:

mesh
fn home_handler(request) do
  HTTP.response(200, "home")
end

fn health_handler(request) do
  HTTP.response(200, json { status: "ok" })
end

fn main() do
  let r = HTTP.router()
  let r = HTTP.route(r, "/", home_handler)
  let r = HTTP.route(r, "/health", health_handler)
  HTTP.serve(r, 8080)
end

Method-Specific Routes ​

Use HTTP.on_get, HTTP.on_post, HTTP.on_put, and HTTP.on_delete to match specific HTTP methods:

mesh
fn me_handler(request) do
  HTTP.response(200, "me")
end

fn user_handler(request) do
  let param = Request.param(request, "id")
  case param do
    Some(id) -> HTTP.response(200, id)
    None -> HTTP.response(400, "no-id")
  end
end

fn post_handler(request) do
  HTTP.response(200, "posted")
end

fn fallback_handler(request) do
  HTTP.response(200, "fallback")
end

fn main() do
  let r = HTTP.router()
  let r = HTTP.on_get(r, "/users/me", me_handler)
  let r = HTTP.on_get(r, "/users/:id", user_handler)
  let r = HTTP.on_post(r, "/data", post_handler)
  let r = HTTP.route(r, "/*", fallback_handler)
  HTTP.serve(r, 8080)
end

Route precedence: static paths like /users/me are matched before parameterized paths like /users/:id. The wildcard /* matches any path not matched by other routes.

Path Parameters ​

Use :param syntax in route paths to capture dynamic segments. Access captured values with Request.param:

mesh
fn user_handler(request) do
  let param = Request.param(request, "id")
  case param do
    Some(id) -> HTTP.response(200, id)
    None -> HTTP.response(400, "missing id")
  end
end

fn main() do
  let r = HTTP.router()
  let r = HTTP.on_get(r, "/users/:id", user_handler)
  HTTP.serve(r, 8080)
end

Request.param returns an Option -- Some(value) if the parameter exists, None otherwise. Use pattern matching to handle both cases.

Clustered Routes ​

In a cluster, wrap a handler in HTTP.clustered to let Mesh choose the node that runs it and record each request for replay:

mesh
pub fn handle_get_account(request :: Request) -> Response do
  case Request.param(request, "id") do
    Some(id) -> HTTP.response(200, json { id: id })
    None -> HTTP.response(400, json { error: "missing id" })
  end
end

pub fn handle_create_account(request :: Request) -> Response do
  HTTP.response(201, Request.body(request))
end

fn main() do
  let router = HTTP.router()
    |> HTTP.on_get("/accounts/:id", HTTP.clustered(1, handle_get_account))
    |> HTTP.on_post("/accounts", HTTP.clustered(2, handle_create_account))
  HTTP.serve(router, 8080)
end

HTTP.clustered(handler) and HTTP.clustered(N, handler) are valid only as the handler argument of HTTP.route or HTTP.on_*. The handler must be a public function named directly or through its module, and N must be a positive integer literal: the total number of copies of each request's record, including the node that runs it. Omitting N uses the manifest's [cluster].default_replicas, or 2 without one, and one handler cannot be registered with two different counts.

The runtime records the request, runs the handler on the selected node, and adds X-Mesh-Continuity-Request-Key, X-Mesh-Ingress-Node, X-Mesh-Execution-Node, and X-Mesh-Routed-Remotely headers to the response. A request carrying an Idempotency-Key header gets a key scoped to the application, handler, and tenant. Repeating a completed request with the same key replays the retained response with Idempotency-Replayed: true; reusing the key for a different request is rejected. When a request cannot be placed, the response is 503 with a JSON error field. A process without peers answers every request to a route with a count above 1 with {"error":"replica_required_unavailable"}. See Autonomous Clusters for replica, replay, and routing rules.

Request Accessors ​

The Request module provides accessors for reading request data:

FunctionReturnsDescription
Request.method(request)StringHTTP method (GET, POST, etc.)
Request.path(request)StringRequest path
Request.body(request)StringRequest body
Request.body_bytes(request)BytesByte-exact request body
Request.header(request, name)Option<String>Header value by name, whatever its case (x-agent finds X-Agent)
Request.query(request, name)Option<String>Query parameter by name
Request.param(request, name)Option<String>Path parameter by name
HTTP.request_id(request)StringRuntime-generated request correlation ID
HTTP.idempotency_key(request)Option<String>Validated idempotency key, when supplied

The server bounds each request: the request line and complete header section are limited to 8 KiB, at most 100 headers are accepted, and the body is limited to 1 MiB. Requests using Transfer-Encoding are rejected; send a bounded Content-Length.

HTTP.idempotency_key reads the case-insensitive Idempotency-Key header. Keys must contain 1–255 visible ASCII bytes with no surrounding whitespace; an invalid key makes the server return 400 before the route handler runs.

Graceful Shutdown ​

Call Process.install_shutdown_signals() before serving to translate native SIGINT and SIGTERM into a shutdown request. HTTP.serve and HTTP.serve_tls then stop accepting new connections and drain connections they already accepted. Application code can trigger the same path with Process.request_shutdown().

Middleware ​

Middleware functions wrap request handling with cross-cutting concerns like logging, authentication, or CORS. Add middleware with HTTP.use:

mesh
fn logger(request :: Request, next) -> Response do
  next(request)
end

fn auth_check(request :: Request, next) do
  let path = Request.path(request)
  let is_secret = String.starts_with(path, "/secret")
  if is_secret do
    HTTP.response(401, "Unauthorized")
  else
    next(request)
  end
end

fn handler(request :: Request) do
  HTTP.response(200, "hello-world")
end

fn secret_handler(request :: Request) do
  HTTP.response(200, "secret-data")
end

fn main() do
  let r = HTTP.router()
  let r = HTTP.use(r, logger)
  let r = HTTP.use(r, auth_check)
  let r = HTTP.route(r, "/hello", handler)
  let r = HTTP.route(r, "/secret", secret_handler)
  HTTP.serve(r, 8080)
end

Middleware Signature ​

A middleware function takes two arguments:

  • request -- the incoming Request
  • next -- a continuation function that passes the request to the next middleware or the final handler

Call next(request) to continue the chain. Return a Response directly (without calling next) to short-circuit the chain, as shown in the auth_check example above.

Middleware runs in the order added with HTTP.use. In the example above, every request passes through logger first, then auth_check, and finally the matched route handler.

JSON ​

JSON Object Literals ​

Use json { } to construct JSON objects without manual string escaping or heredoc interpolation. Passed as a String argument, the result is its JSON text, so it goes directly to HTTP.response:

mesh
fn api_handler(request) do
  HTTP.response(200, json { status: "ok", count: 42 })
end

fn error_handler(request) do
  HTTP.response(400, json { error: "bad request" })
end

Values are serialized based on their Mesh type: String → quoted, Int/Float → unquoted number, Bool → true/false, nil → null, Option<T> → null or value, List<T> → array, struct with deriving(Json) → nested object. See JSON Literals in the Language Basics guide for the full type table.

Nested json { } values embed raw — no double-encoding:

mesh
let inner = json { code: 200 }
let outer = json { result: inner, ok: true }
# outer is: {"result":{"code":200},"ok":true}

Note: Keys must be bare identifiers. Reserved keywords (type, fn, let, etc.) cannot be used as keys — use heredoc strings for JSON objects with keyword-named fields.

Json Module ​

Json.parse creates a structured Json value. Navigation and scalar conversion are checked, so a missing field, out-of-range array index, or unexpected type returns Err. Every step returns a Result, so ? chains them:

mesh
fn first_user_name(body :: String) -> String!String do
  let root = Json.parse(body)?
  let users = Json.object_get(root, "users")?
  let user = Json.array_get(users, 0)?
  let name = Json.object_get(user, "name")?
  Json.as_string(name)
end

fn main() do
  case first_user_name("{\"users\":[{\"name\":\"Ada\"}],\"cursor\":null}") do
    Ok(name) -> println(name)
    Err(error) -> println(error)
  end
end

JSON is an alias of the same module. Json.encode also takes any value JSON can hold: Int, Float, Bool, String, tuples (as arrays), and Option (None is null), List and Map<String, V> of them, and types with deriving(Json). Object keys are written in the object's own order: a json { } literal's as written, a struct's fields as declared, a Map's as it iterates (insertion order), and a parsed object's as the text had them (a repeated key keeps its first place and its last value).

FunctionReturnsDescription
Json.parse(text)Result<Json, String>Parse one JSON value
Json.encode(value)StringEncode a Json value or a value with JSON support
Json.encode_string(value)StringEncode a JSON string scalar
Json.encode_int(value)StringEncode an integer scalar
Json.encode_bool(value)StringEncode a boolean scalar
Json.encode_map(value)StringEncode a map
Json.encode_list(value)StringEncode a list
Json.object_get(value, key)Result<Json, String>Read an object member
Json.array_get(value, index)Result<Json, String>Read a checked zero-based array element
Json.array_length(value)Result<Int, String>Read an array's length
Json.is_null(value)BoolTest for JSON null
Json.as_string(value)Result<String, String>Require a JSON string
Json.as_int(value)Result<Int, String>Require an integral JSON number in Int range
Json.as_float(value)Result<Float, String>Require a JSON number
Json.as_bool(value)Result<Bool, String>Require a JSON boolean

The older Json.get(json_text, key), Json.get_nested(json_text, first, second), and Json.is_string(json_text, key) helpers operate on JSON text and return lossy scalar strings. Prefer Json.parse plus the checked structured accessors for new code.

Struct Serialization with deriving(Json) ​

Structs that derive Json get automatic to_json and from_json methods:

mesh
struct User do
  name :: String
  age :: Int
  active :: Bool
end deriving(Json)

fn main() do
  # Encode to JSON string
  let user = User { name: "Alice", age: 30, active: true }
  let json_str = Json.encode(user)
  println(json_str)

  # Decode from JSON string
  let result = User.from_json("{\"name\":\"Bob\",\"age\":25,\"active\":false}")
  case result do
    Ok(u) -> println("${u.name}")
    Err(e) -> println("Error: ${e}")
  end
end

For HTTP handlers, combine JSON encoding with HTTP.response to return JSON responses:

mesh
fn api_handler(request) do
  let body = Request.body(request)
  # Process the JSON body...
  HTTP.response(200, json { status: "ok" })
end

WebSocket ​

Mesh includes a built-in WebSocket server for real-time bidirectional communication. Create a WebSocket server with Ws.serve, providing three lifecycle callbacks:

mesh
fn on_connect(conn, path, headers) do
  println("connected on #{path}")
  Ws.send(conn, "Welcome!")
  1
end

fn on_message(conn, msg) do
  Ws.send(conn, msg)
end

fn on_close(conn, code, reason) do
  println("client disconnected: #{code} #{reason}")
end

fn main() do
  Ws.serve(on_connect, on_message, on_close, 9001)
end

Lifecycle Callbacks ​

CallbackArgumentsPurpose
on_connect(conn, path, headers)Called after the upgrade with the request path and Map<String, String> headers; return nonzero to accept or 0 to reject with close code 1008
on_message(conn, msg)Called for each message from the client. Its return value is discarded.
on_close(conn, code, reason)Called with the close code and reason. Cleanup is automatic, and the return value is discarded.

Each WebSocket connection runs as an isolated actor. If a handler crashes, only that connection is affected -- the server continues accepting new connections.

Ws.serve starts its accept loop on a background thread and returns after binding the port. Keep the native process alive with your HTTP server or other application work.

Sending Messages ​

Use Ws.send to send a text message to a specific connection:

mesh
fn on_message(conn, msg) do
  let status = Ws.send(conn, "Echo: " <> msg)
  if status != 0 do
    println("send failed")
  end
end

Rooms and Broadcasting ​

Rooms provide pub/sub messaging. Connections can join named rooms and broadcast messages to all room members:

mesh
fn on_connect(conn, path, headers) do
  Ws.join(conn, "lobby")
  Ws.send(conn, "Welcome to the lobby!")
  1
end

fn on_message(conn, msg) do
  # Broadcast to all connections in the room
  Ws.broadcast("lobby", msg)
end

fn on_close(conn, code, reason) do
  # Room membership is automatically cleaned up on disconnect
  println("client left")
end

fn main() do
  Ws.serve(on_connect, on_message, on_close, 9001)
end
FunctionReturnsDescription
Ws.send(conn, message)IntSend text; 0 indicates success
Ws.join(conn, room)IntSubscribe a connection to a named room
Ws.leave(conn, room)IntUnsubscribe a connection from a room
Ws.broadcast(room, msg)IntSend to every room member; returns the local write-failure count
Ws.broadcast_except(room, msg, conn)IntSend to all room members except one local connection

Room membership is automatically cleaned up when a connection disconnects -- you do not need to manually call Ws.leave in the on_close callback.

A failed room write disconnects that recipient, allowing it to reconnect and recover missed data. Applications must perform catch-up after subscribing; room broadcasts are notifications, not durable delivery acknowledgements.

In a distributed cluster, Ws.broadcast automatically forwards messages to room members on other nodes.

WebSocket Client ​

WsClient is the outbound client surface. It accepts ws:// and certificate-validated wss:// URLs, yields while connecting or waiting for a message, and bounds both message size and the inbound queue.

mesh
fn exchange(url :: String, options :: Int) -> Int!String do
  let connection = WsClient.connect(url, options)?
  WsClient.send_text(connection, "subscribe")?
  let frame = Bytes.from_hex("0102")?
  WsClient.send_bytes(connection, frame)?

  let message = WsClient.recv(connection, 5_000)?
  if message.kind == "text" do
    case Bytes.to_utf8(message.data) do
      Ok(text) -> println(text)
      Err(error) -> println(error)
    end
  else if message.kind == "binary" do
    println(Bytes.to_hex(message.data))
  else
    println("closed: #{message.close_code}:#{message.close_reason}")
  end

  WsClient.close(connection, 1_000, "done")?
  Ok(0)
end

fn main() do
  let options = WsClient.options()
    |> WsClient.connect_timeout(5_000)
    |> WsClient.heartbeat_timeout(30_000)
    |> WsClient.max_message_bytes(1_048_576)
    |> WsClient.queue_capacity(256)

  case exchange("wss://example.com/feed", options) do
    Ok(_) -> println("done")
    Err(error) -> println(error)
  end
end
FunctionReturnsDescription
WsClient.options()IntCreate a single-use options handle
WsClient.connect_timeout(options, ms)IntSet the DNS/TCP/TLS/upgrade timeout
WsClient.heartbeat_timeout(options, ms)IntSet the ping/pong liveness timeout
WsClient.max_message_bytes(options, bytes)IntBound inbound and outbound text or binary messages, including fragments
WsClient.queue_capacity(options, messages)IntBound unread inbound messages
WsClient.connect(url, options)Result<Int, String>Connect, consume the options handle, and return a connection handle
WsClient.send_text(connection, text)Result<Unit, String>Send a masked text frame
WsClient.send_bytes(connection, bytes)Result<Unit, String>Send a masked binary frame
WsClient.recv(connection, timeout_ms)Result<WsMessage, String>Receive the next message; only one receiver may wait per connection
WsClient.close(connection, code, reason)Result<Unit, String>Send a close frame and release the handle
WsClient.reconnect_delay(attempt, base_ms, max_ms, jitter_ppm)Result<Int, String>Return a bounded exponential backoff delay in milliseconds, with jitter

Options default to a 10-second connect timeout, 30-second heartbeat timeout, 1 MiB message limit, and 256-message queue. Connect timeouts must be 1–120,000 ms, heartbeat timeouts 1,000–300,000 ms, message limits 1 byte–16 MiB, and queue capacities 1–65,536. Setters retain the fluent handle; connect performs validation and consumes it.

WsMessage has four fields:

FieldTypeMeaning
kindString"text", "binary", or "close"
dataBytesPayload bytes, with text as UTF-8; empty for "close"
close_codeIntClose code for "close", otherwise 0
close_reasonStringClose reason for "close", otherwise empty

WsClient.recv returns Err("TIMEOUT: WebSocket receive") when no message arrives within timeout_ms; a negative timeout is an error. Queue overflow closes the connection with a BACKPRESSURE error rather than dropping data silently. Heartbeat timeout and other disconnects are observable errors.

Reconnect is deliberately explicit. After an interruption, the caller chooses whether to reconnect, uses reconnect_delay, restores subscriptions, and checks source sequence numbers for regressions. The helper doubles base_ms for each attempt up to max_ms, then picks a delay within jitter_ppm parts per million of that value, never above max_ms. It accepts attempts 0..62, positive base_ms <= max_ms, and jitter from 0 to 1_000_000; other arguments return Err. The runtime never restores subscriptions or retries writes implicitly.

TLS ​

HTTP.serve_tls serves HTTPS with a PEM certificate and private key:

mesh
fn handler(request) do
  HTTP.response(200, "Secure hello!")
end

fn main() do
  let r = HTTP.router()
  let r = HTTP.route(r, "/", handler)
  HTTP.serve_tls(r, 8443, "cert.pem", "key.pem")
end

The server performs TLS negotiation with rustls. Ws.serve_tls(on_connect, on_message, on_close, port, "cert.pem", "key.pem") serves wss:// the same way; certificates it cannot load are reported and it returns without serving. The outbound WsClient supports certificate-validated wss:// connections directly.

HTTP Client ​

Mesh provides a fluent builder API for making outbound HTTP requests via the Http module (note: lowercase Http, distinct from the HTTP server module).

Fluent Builder ​

Builder functions return the same single-use request handle, so pipe them:

mesh
fn main() do
  let request = Http.build(:get, "https://api.example.com/data")
    |> Http.header("Authorization", "Bearer token")
    |> Http.query("market", "SOL/USDC")
    |> Http.timeout(30_000)
    |> Http.stage_timeout(:resolve, 2_000)
    |> Http.stage_timeout(:connect, 5_000)
    |> Http.stage_timeout(:send, 5_000)
    |> Http.stage_timeout(:first_byte, 10_000)
    |> Http.stage_timeout(:body, 10_000)
    |> Http.max_redirects(5)
    |> Http.max_response_bytes(1_048_576)

  case Http.send(request) do
    Ok(response) -> println("#{response.status}: #{response.body}")
    Err(error) -> println("error: #{error}")
  end
end
FunctionDescription
Http.build(method, url)Create a request for :get, :head, :post, :put, :patch, :delete, or :options
Http.header(req, key, value)Add a request header
Http.query(req, key, value)Add a percent-encoded query parameter
Http.body(req, value)Set a POST, PUT, or PATCH body
Http.body_bytes(req, value)Set a byte-exact POST, PUT, or PATCH body
Http.json(req, value)Set a body and the JSON content type
Http.timeout(req, ms)Set the total timeout
Http.stage_timeout(req, stage, ms)Set :resolve, :connect, :send, :first_byte, or :body timeout
Http.max_redirects(req, count)Set the request-local redirect limit; 0 disables redirects
Http.max_response_bytes(req, bytes)Set the buffered or streamed response limit
Http.send(req)Yield while executing; return Result<HttpResponse, String>

HttpResponse has status, body, body_bytes, and headers fields. body_bytes always preserves the exact response bytes; body is empty when the payload is not valid UTF-8. HTTP status errors such as 404 are successful protocol responses and therefore return Ok; inspect status. Network, timeout, and body-limit failures return Err.

Timeouts must be between 1 and 120,000 milliseconds. Requests follow the HTTP client's default of up to 10 redirects unless Http.max_redirects sets a request-local limit from 0 through 20. Responses default to an 8 MiB limit and cannot be configured above 64 MiB.

POST Requests ​

mesh
fn main() do
  let request = Http.build(:post, "https://api.example.com/items")
    |> Http.json(json { name: "widget", price: 9 })
    |> Http.max_response_bytes(65_536)

  case Http.send(request) do
    Ok(response) -> println("created: #{response.body}")
    Err(error) -> println("error: #{error}")
  end
end

Streaming ​

Http.stream emits valid UTF-8 String chunks. Http.stream_bytes emits binary-safe Bytes. Each callback completes before the next bounded 8 KiB chunk is read, providing backpressure.

mesh
fn main() do
  let handle = Http.build(:get, "https://example.com/large-file")
    |> Http.max_response_bytes(8_388_608)
    |> Http.stream_bytes(fn chunk do
      println(Bytes.to_hex(chunk))
      "continue"
    end)

  Timer.sleep(100)
  Http.cancel(handle)
end

Return "stop" from a callback to end the stream, or pass the returned handle to Http.cancel. Streaming begins on a dedicated I/O thread and does not block a scheduler worker. The callback API has no error channel; a setup or read failure ends the stream and is visible through Http.metrics.

Cancellation ​

The request handle is also its cancellation handle. Retain a copy and send it to another actor before calling Http.send or Http.send_with; that actor can call Http.cancel(request) while the caller is suspended. Cancellation wakes the caller immediately. The bounded stage timeout still limits any underlying OS I/O that is already in progress.

Calling Http.cancel or Http.client_close more than once is safe.

Keep-Alive Client ​

Reuse one connection pool across requests to the same origin:

mesh
fn fetch(client :: Int, url :: String) do
  case Http.build(:get, url) |2> Http.send_with(client) do
    Ok(response) -> println(response.body)
    Err(error) -> println(error)
  end
end

fn main() do
  let client = Http.client()
  fetch(client, "https://api.example.com/data")
  fetch(client, "https://api.example.com/health")
  Http.client_close(client)
end
FunctionDescription
Http.client()Create a keep-alive HTTP client handle
Http.send_with(client, req)Send request reusing the client's connection pool
Http.stream(req, callback)Stream UTF-8 text with callback backpressure
Http.stream_bytes(req, callback)Stream Bytes with callback backpressure
Http.cancel(handle)Cancel a pending/active request or stream
Http.client_close(client)Close the client and release connections

Retries and Metrics ​

Mesh never retries HTTP requests automatically. Http.retry_class(method, error) returns "safe_retry" for transient GET, HEAD, and OPTIONS failures, "unsafe_retry" for transient writes, and "do_not_retry" for permanent failures. The caller owns retry count, backoff, and idempotency policy.

Http.metrics() returns cumulative process-wide metrics:

FieldMeaning
requests, in_flight, cancellationsRequest count, current gauge, and cancellation count
dns_micros, connect_micros, tls_microsTime spent creating new connections; pooled requests do not repeat these stages
first_byte_micros, total_microsCumulative time to first byte and total request time
dns_failures, connect_failures, tls_failures, timeoutsClassified failure counts
response_bytesSuccessfully read or streamed bytes

What's Next? ​

  • Databases -- SQLite, PostgreSQL, connection pooling, and struct mapping
  • Concurrency -- actors, message passing, and supervision trees
  • Syntax Cheatsheet -- quick reference for all Mesh syntax
Edit this page on GitHub
v0.1.8 Last updated: September 28, 2026