Mako provides a systems-level HTTP stack: synchronous, one-request-at-a-time per connection, with no colored async. You scale concurrency by running handlers inside crew blocks. This chapter covers TCP, HTTP/1.1, HTTPS, HTTP/2, WebSockets, REST APIs, and request routing patterns.
At the lowest level, Mako provides raw TCP socket operations:
fn main() {
// Server side
let fd = tcp_listen(18082)
let client = tcp_accept(fd)
let _ = tcp_write(client, "hello from server\n")
let _ = tcp_close(client)
let _ = tcp_close(fd)
}
fn main() {
// Client side
let peer = tcp_connect("127.0.0.1", 18082)
let data = tcp_read(peer)
print(data)
let _ = tcp_close(peer)
}
TCP operations block the calling thread. Use crew blocks to handle multiple connections concurrently.
The HTTP server API is synchronous and explicit. You bind a port, accept connections in a loop, inspect the request, send a response, and close.
fn main() {
let fd = http_bind(18100)
if fd < 0 {
print("bind failed")
return
}
print("listening on :18100")
let c = http_accept(fd)
if c >= 0 {
let _ = http_respond(c, 200, "hello from mako\n")
let _ = http_close(c)
}
let _ = http_close_listener(fd)
}
| Function | Purpose |
|---|---|
http_bind(port) |
Bind and listen on a TCP port. Returns listener fd (< 0 on error). |
http_accept(fd) |
Accept one connection, parse the HTTP request. Returns connection handle. |
http_method(c) |
Get the request method (GET, POST, PUT, DELETE, etc.). |
http_path(c) |
Get the request path (e.g., "/users/42"). |
http_body(c) |
Get the request body as a string. |
http_header(c, name) |
Get a specific request header value. |
http_respond(c, status, body) |
Send response with text/plain content type. |
http_respond_ct(c, status, content_type, body) |
Send response with explicit content type. |
http_respond_json(c, status, json) |
Send response with application/json content type. |
http_close(c) |
Close the connection (frees the slot). |
http_close_listener(fd) |
Close the listening socket. |
fn main() {
let fd = http_bind(18100)
if fd < 0 {
print("bind failed")
return
}
print("http_server on :18100")
let mut n = 0
while n < 50 {
let c = http_accept(fd)
if c < 0 {
// accept failed, skip
} else {
let p = http_path(c)
if str_eq(p, "/health") {
let _ = http_respond_ct(
c,
200,
"application/json",
"{\"ok\":true}\n"
)
} else {
if str_eq(p, "/") {
let _ = http_respond(c, 200, "hello from mako\n")
} else {
let _ = http_respond(c, 404, "not found\n")
}
}
let _ = http_close(c)
n = n + 1
}
}
let _ = http_close_listener(fd)
print("server done")
}
let c = http_accept(fd)
let method = http_method(c)
if str_eq(method, "POST") {
// handle POST
} else {
if str_eq(method, "GET") {
// handle GET
}
}
let path = http_path(c)
// path is the raw URI path, e.g. "/users/42"
let body = http_body(c)
// body contains the raw request body (up to Content-Length or 1MB max)
let host = http_header(c, "Host")
let ua = http_header(c, "User-Agent")
let ct = http_header(c, "Content-Type")
print(host)
print(ua)
print(ct)
Header lookup is case-insensitive. The runtime validates header names and values, rejecting CR/LF/NUL to prevent header injection attacks.
let _ = http_respond(c, 200, "OK\n")
let _ = http_respond_ct(c, 200, "text/html", "<h1>Hello</h1>")
let _ = http_respond_json(c, 200, "{\"status\":\"ok\"}")
This is equivalent to http_respond_ct(c, 200, "application/json", body).
The runtime maps standard status codes to reason phrases automatically:
| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 405 | Method Not Allowed |
| 500 | Internal Server Error |
fn handle_request(c: int) {
let method = http_method(c)
let path = http_path(c)
if str_eq(path, "/") {
let _ = http_respond(c, 200, "home\n")
} else {
if str_eq(path, "/health") {
let _ = http_respond_json(c, 200, "{\"ok\":true}\n")
} else {
if str_eq(path, "/api/users") {
if str_eq(method, "GET") {
let _ = http_respond_json(c, 200, "[]\n")
} else {
if str_eq(method, "POST") {
let body = http_body(c)
let _ = http_respond_json(c, 201, body)
} else {
let _ = http_respond(c, 405, "method not allowed\n")
}
}
} else {
let _ = http_respond(c, 404, "not found\n")
}
}
}
}
fn main() {
let fd = http_bind(18090)
if fd < 0 {
print("bind failed")
return
}
print("routes on :18090")
while 1 == 1 {
let c = http_accept(fd)
if c >= 0 {
handle_request(c)
let _ = http_close(c)
}
}
}
fn handle_request(c: int) {
let path = http_path(c)
if str_contains(path, "/api/") {
// API routes
if str_eq(path, "/api/status") {
let _ = http_respond_json(c, 200, "{\"status\":\"running\"}\n")
} else {
let _ = http_respond_json(c, 404, "{\"error\":\"not found\"}\n")
}
} else {
// Static/page routes
let _ = http_respond(c, 200, "page\n")
}
}
Use crew blocks to serve multiple requests in parallel:
fn handle_connection(fd: int) -> int {
let c = http_accept(fd)
if c >= 0 {
let p = http_path(c)
if str_eq(p, "/slow") {
sleep_ms(100)
let _ = http_respond(c, 200, "done\n")
} else {
let _ = http_respond(c, 200, "fast\n")
}
let _ = http_close(c)
}
return 0
}
fn main() {
let fd = http_bind(18100)
if fd < 0 {
print("bind failed")
return
}
crew t {
// Kick multiple handlers — each accepts one connection
let h1 = t.kick(handle_connection(fd))
let h2 = t.kick(handle_connection(fd))
let h3 = t.kick(handle_connection(fd))
let _ = h1.join()
let _ = h2.join()
let _ = h3.join()
}
let _ = http_close_listener(fd)
}
When OpenSSL is linked, Mako supports HTTPS with TLS termination:
fn main() {
let code = tls_serve_n(
18443,
"runtime/certs/dev.crt",
"runtime/certs/dev.key",
"hello from mako https\n",
3 // serve 3 requests then exit
)
if code != 0 {
print("tls_serve_n failed (OpenSSL missing or bind error)")
return
}
print("https_server done")
}
runtime/certs/dev.crt).fn main() {
let result = tls_connect("example.com", 443)
if result >= 0 {
print("connected via TLS")
}
}
HTTP/2 support uses TLS with ALPN h2 negotiation:
fn main() {
let code = tls_serve_h2_routes(
18446,
"runtime/certs/dev.crt",
"runtime/certs/dev.key",
"hello from mako h2\n", // body for /
"{\"ok\":true}\n", // body for /health
2 // max requests
)
if code != 0 {
print("h2 server failed (OpenSSL missing or bind error)")
return
}
print("h2_server done")
}
HTTP/2 features:
- Multiplexed streams over a single TLS connection
- HPACK header compression
- ALPN protocol negotiation (h2)
Test with: curl -sk --http2 https://127.0.0.1:18446/health
Mako provides WebSocket server support with RFC6455 upgrade, text/binary frames, and automatic ping/pong handling.
fn main() {
// Starts a WebSocket server that echoes one client's messages then exits
print_int(ws_echo_once(18092))
}
The ws_echo_once builtin:
1. Binds the specified port
2. Accepts one TCP connection
3. Performs the WebSocket upgrade handshake (SHA-1 + base64 accept key)
4. Echoes text and binary frames back to the client
5. Responds to ping frames with pong automatically
6. Closes when the client disconnects or sends a close frame
fn main() {
// WebSocket server with ping/pong support
print_int(ws_echo_once(18095))
}
WebSocket ping/pong is handled automatically by the runtime. When a ping frame arrives, a pong frame with the same payload is sent back immediately.
Sec-WebSocket-Key + magic GUID, SHA-1 hashed,
base64 encoded.A complete REST API example combining routing, JSON, and proper HTTP methods:
fn handle_users_get(c: int) {
let _ = http_respond_json(c, 200, "[{\"id\":1,\"name\":\"Ada\"}]\n")
}
fn handle_users_post(c: int) {
let body = http_body(c)
let name = json_get_string(body, "name")
if len(name) == 0 {
let _ = http_respond_json(c, 400, "{\"error\":\"name required\"}\n")
} else {
let resp = json_ss("id", "2", "name", name)
let _ = http_respond_json(c, 201, resp)
}
}
fn handle_health(c: int) {
let _ = http_respond_json(c, 200, "{\"status\":\"healthy\"}\n")
}
fn route(c: int) {
let method = http_method(c)
let path = http_path(c)
if str_eq(path, "/health") {
handle_health(c)
} else {
if str_eq(path, "/api/users") {
if str_eq(method, "GET") {
handle_users_get(c)
} else {
if str_eq(method, "POST") {
handle_users_post(c)
} else {
let _ = http_respond(c, 405, "method not allowed\n")
}
}
} else {
let _ = http_respond_json(c, 404, "{\"error\":\"not found\"}\n")
}
}
}
fn main() {
let fd = http_bind(18100)
if fd < 0 {
print("bind failed")
return
}
print("REST API on :18100")
let mut n = 0
while n < 100 {
let c = http_accept(fd)
if c >= 0 {
route(c)
let _ = http_close(c)
n = n + 1
}
}
let _ = http_close_listener(fd)
}
# Health check
curl -s http://127.0.0.1:18100/health
# List users
curl -s http://127.0.0.1:18100/api/users
# Create user
curl -s -X POST http://127.0.0.1:18100/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Grace"}'
# 404
curl -s http://127.0.0.1:18100/api/unknown
The runtime provides shutdown coordination for long-running servers:
fn main() {
let fd = http_bind(18100)
if fd < 0 {
return
}
// Begin graceful shutdown with 5-second grace period
let deadline = http_shutdown_begin(5000)
// Check shutdown state
if http_shutdown_requested() == 1 {
// Drain in-flight requests
while http_active_connections() > 0 {
if http_shutdown_expired() == 1 {
break
}
sleep_ms(100)
}
}
let _ = http_close_listener(fd)
}
Shutdown API:
| Function | Purpose |
|---|---|
http_shutdown_begin(grace_ms) |
Start graceful shutdown with grace period |
http_shutdown_requested() |
Check if shutdown was requested (returns 1 or 0) |
http_shutdown_ready() |
Check if server is ready to accept (returns 1 or 0) |
http_shutdown_remaining() |
Milliseconds until grace period expires |
http_shutdown_expired() |
Check if grace period has elapsed |
http_active_connections() |
Count of currently active connection slots |
http_shutdown_reset() |
Reset shutdown state (for testing) |
| Concern | Practice |
|---|---|
| Header injection | Runtime validates all header names/values, rejects CR/LF/NUL |
| Content-Length | Enforced automatically on responses |
| Request body limits | 1MB default (or Content-Length, whichever is smaller) |
| Secrets in memory | Use secret_from_str / secret_drop |
| TLS in production | Use trusted CA certificates; never disable verification |
| Concurrent safety | Each connection fd is independent; do not share across tasks |
| Header validation | Use http_header_ok for custom header checks |
fn main() {
let fd = http_bind(18100)
if fd < 0 {
return
}
let c = http_accept(fd)
if c >= 0 {
let auth = http_header(c, "Authorization")
// Header values are already validated by the runtime
// (no CR/LF/NUL injection possible)
if len(auth) == 0 {
let _ = http_respond(c, 401, "unauthorized\n")
} else {
let _ = http_respond(c, 200, "ok\n")
}
let _ = http_close(c)
}
let _ = http_close_listener(fd)
}
Client-side HTTP helpers for making outbound requests:
fn main() {
let body = http_get("http://127.0.0.1:18100/health")
print(body)
}
For production use: - Always set explicit timeouts - Validate URLs before making requests - Use parameterized URL construction (no string concatenation of user input)
HTTP/1.1 connections support keep-alive by default. The runtime detects the
Connection: keep-alive header and maintains the connection slot:
fn main() {
let fd = http_bind(18100)
if fd < 0 {
return
}
// The runtime automatically handles Connection: keep-alive
// and Connection: close headers on incoming requests.
// Use http_close(c) when you want to force-close regardless.
let c = http_accept(fd)
if c >= 0 {
let _ = http_respond(c, 200, "first response\n")
// For one-shot servers, close explicitly:
let _ = http_close(c)
}
let _ = http_close_listener(fd)
}
The runtime maintains a fixed connection table (32 slots by default). Each
http_accept occupies one slot; http_close frees it. If all slots are in use,
http_accept blocks until a slot becomes available.
Design accordingly: - Close connections promptly after responding - Use crew blocks to serve connections concurrently within the slot limit - For high-concurrency scenarios, dispatch to worker tasks quickly
For servers that must handle many concurrent connections without a thread per
connection, Mako provides a non-blocking event loop (runtime/mako_evloop.h).
It uses epoll on Linux and kqueue on macOS under the hood.
fn main() {
let el = evloop_new()
let server_fd = nb_listen(8080)
let _ = evloop_add(el, server_fd, 1) // monitor for readability
while true {
let n = evloop_wait(el, 1000)
let mut i = 0
while i < n {
let fd = evloop_event_fd(el, i)
if fd == server_fd {
let client = nb_accept(server_fd)
if client >= 0 {
let _ = evloop_add(el, client, 1)
}
} else {
let data = nb_read(fd)
if len(data) > 0 {
let _ = nb_write(fd, "echo: " + data)
}
let _ = evloop_del(el, fd)
let _ = nb_close(fd)
}
i = i + 1
}
}
let _ = evloop_close(el)
}
| Function | Purpose |
|---|---|
evloop_new() |
Create event loop |
evloop_add(el, fd, flags) |
Register fd |
evloop_mod(el, fd, flags) |
Modify interest flags |
evloop_del(el, fd) |
Remove fd |
evloop_wait(el, timeout_ms) |
Wait for events, returns count |
evloop_event_fd(el, index) |
Get fd from event at index |
evloop_event_flags(el, index) |
Get flags from event at index |
evloop_close(el) |
Destroy event loop |
| Function | Purpose |
|---|---|
nb_listen(port) |
Create non-blocking TCP listener |
nb_accept(fd) |
Non-blocking accept |
nb_read(fd) |
Non-blocking read (returns available data) |
nb_write(fd, data) |
Non-blocking write |
nb_udp_bind(port) |
Non-blocking UDP socket |
nb_udp_recv(fd) |
Non-blocking UDP receive |
nb_close(fd) |
Close non-blocking socket |
For real-time game servers, Mako provides a dedicated UDP networking subsystem
(runtime/mako_game.h) that tracks connected peers and supports broadcast:
fn main() {
let u = game_udp_bind(27015)
let el = evloop_new()
let _ = evloop_add(el, game_udp_fd(u), 1)
let interval_us = 16666 // ~60 Hz tick rate
while true {
let start = tick_now_us()
let n = evloop_wait(el, 1)
if n > 0 {
let data = game_udp_recv(u)
let peer = game_udp_sender(u)
// Process input from peer, then broadcast state
let _ = game_udp_broadcast(u, "world_state")
}
let _ = tick_sleep_us(start, interval_us)
}
game_udp_close(u)
let _ = evloop_close(el)
}
| Function | Purpose |
|---|---|
game_udp_bind(port) |
Bind UDP game socket |
game_udp_recv(u) |
Receive packet (tracks sender) |
game_udp_sender(u) |
Get peer ID of last sender |
game_udp_send(u, peer, data) |
Send to specific peer |
game_udp_broadcast(u, data) |
Send to all connected peers |
game_udp_kick(u, peer) |
Disconnect a peer |
game_udp_peers(u) |
Number of connected peers |
game_udp_fd(u) |
Raw fd for event loop integration |
game_udp_close(u) |
Close socket |
tick_now_us() |
Microsecond timestamp |
tick_sleep_us(start, interval) |
Sleep to maintain tick rate |
The game_udp_fd function returns the raw file descriptor so you can integrate
the game socket into an event loop alongside other I/O sources.
For applications that want declarative route registration instead of manual
path matching, Mako provides an HTTP engine (runtime/mako_httpengine.h):
fn main() {
let e = httpengine_new()
let _ = httpengine_route(e, "GET", "/health", 1)
let _ = httpengine_route(e, "GET", "/api/users", 2)
let _ = httpengine_route(e, "POST", "/api/users", 3)
let _ = httpengine_start(e, 8080)
// Engine runs, dispatching requests to handler IDs
httpengine_stop(e)
httpengine_free(e)
}
| Function | Purpose |
|---|---|
httpengine_new() |
Create HTTP engine instance |
httpengine_route(e, method, path, handler_id) |
Register a route with a handler ID |
httpengine_start(e, port) |
Start listening and serving |
httpengine_stop(e) |
Stop the engine |
httpengine_free(e) |
Destroy the engine |
The engine builds on the event loop internally for non-blocking I/O. Use it
when you want a higher-level abstraction over the raw http_bind/http_accept
loop.
Mako provides built-in session management and authentication primitives that
integrate with the HTTP server. These live in runtime/mako_security.h and
use constant-time comparisons throughout to prevent timing attacks.
Use CMap as a thread-safe session store. Session IDs are generated with
cryptographic randomness, and cookies default to HttpOnly + SameSite=Lax:
let sessions = cmap_new()
fn handle_login(c: int) {
let body = http_body(c)
let user = json_get_string(body, "user")
let pass = json_get_string(body, "pass")
// verify credentials against your database
let sid = session_id_new() // 32-char hex, crypto-random
cmap_set(sessions, sid, user)
let cookie = cookie_make("sid", sid, 86400) // HttpOnly; SameSite=Lax; Path=/; 24h
let _ = http_respond_ct(c, 200, "application/json", "{\"ok\":true}", cookie)
}
fn handle_protected(c: int) {
let cookie_hdr = http_header(c, "Cookie")
let sid = cookie_get(cookie_hdr, "sid")
if cmap_has(sessions, sid) == 0 {
let _ = http_respond_json(c, 401, "{\"error\":\"unauthorized\"}")
return
}
let user = cmap_get(sessions, sid)
let _ = http_respond_json(c, 200, json_object("user", user))
}
fn handle_logout(c: int) {
let cookie_hdr = http_header(c, "Cookie")
let sid = cookie_get(cookie_hdr, "sid")
let _ = cmap_del(sessions, sid)
let expired = cookie_make("sid", "", 0) // expire the cookie
let _ = http_respond_ct(c, 200, "application/json", "{\"ok\":true}", expired)
}
For API-to-API or mobile clients that send tokens in the Authorization header:
fn handle_api(c: int) {
let auth_hdr = http_header(c, "Authorization")
if auth_check_bearer(auth_hdr, expected_api_key) == 0 {
let _ = http_respond_json(c, 401, "{\"error\":\"invalid token\"}")
return
}
// authorized -- proceed
let _ = http_respond_json(c, 200, "{\"data\":\"secret\"}")
}
For browser-facing forms, generate a CSRF token per session and verify it on state-changing requests:
fn handle_form_page(c: int) {
let token = csrf_token()
// store token in session
let cookie_hdr = http_header(c, "Cookie")
let sid = cookie_get(cookie_hdr, "sid")
cmap_set(csrf_store, sid, token)
// include token in the HTML form as a hidden field
let html = "<form><input type=\"hidden\" name=\"csrf\" value=\"" + token + "\">...</form>"
let _ = http_respond_ct(c, 200, "text/html", html)
}
fn handle_form_submit(c: int) {
let cookie_hdr = http_header(c, "Cookie")
let sid = cookie_get(cookie_hdr, "sid")
let expected = cmap_get(csrf_store, sid)
let body = http_body(c)
let submitted = json_get_string(body, "csrf")
if csrf_check(expected, submitted) == 0 {
let _ = http_respond_json(c, 403, "{\"error\":\"CSRF token mismatch\"}")
return
}
// CSRF valid -- process form
}
| Function | Purpose |
|---|---|
cookie_get(header, name) |
Parse cookie value from Cookie header |
cookie_make(name, value, max_age) |
Create Set-Cookie string (HttpOnly, SameSite=Lax) |
session_id_new() |
32-char crypto-random hex session ID |
auth_session_cookie(header, name, expected) |
Constant-time session cookie check |
csrf_token() |
Generate random CSRF token |
csrf_check(expected, submitted) |
Constant-time CSRF comparison |
auth_bearer(authorization) |
Extract token from Bearer header |
auth_check_bearer(authorization, expected) |
Constant-time bearer verification |
auth_basic_header(user, pass) |
Build Basic auth header |
auth_check_basic(authorization, user, pass) |
Verify Basic auth credentials |
auth_token_sign(subject, secret) |
HMAC-SHA256 sign: "subject.signature" |
auth_token_check(token, secret) |
Verify signed token |
auth_token_subject(token) |
Extract subject from signed token |
auth_role_has(roles_csv, role) |
Check role membership |
authz_allow_role(user_roles, required_roles) |
Check if user has any required role |
| Layer | Functions |
|---|---|
| TCP | tcp_listen, tcp_accept, tcp_connect, tcp_read, tcp_write, tcp_close |
| HTTP Server | http_bind, http_accept, http_method, http_path, http_body, http_header |
| HTTP Response | http_respond, http_respond_ct, http_respond_json, http_close |
| HTTPS | tls_serve_n, tls_serve_h2_routes, tls_connect |
| WebSocket | ws_echo_once |
| Event Loop | evloop_new, evloop_add, evloop_wait, evloop_event_fd, evloop_close |
| Non-blocking I/O | nb_listen, nb_accept, nb_read, nb_write, nb_close |
| Game UDP | game_udp_bind, game_udp_recv, game_udp_send, game_udp_broadcast |
| HTTP Engine | httpengine_new, httpengine_route, httpengine_start, httpengine_stop |
| Shutdown | http_shutdown_begin, http_shutdown_requested, http_active_connections |
| Sessions/Auth | session_id_new, cookie_get, cookie_make, auth_bearer, auth_check_bearer, csrf_token, csrf_check |
| Listener | http_close_listener |
Next: Data.