HTTP servers

Testable handlers, bounded streaming, and service loops.

Start with a plain request handler. A handler receives a Request and returns a Response, so the routing and application logic can be tested without opening a port.

use std.http.Response
use std.http_server.Request

fn handle(request: Request) -> Response {
  if request.method == "GET" && request.path == "/health" {
    std.http_server.json(200, "{\"ok\":true}")
  } else if request.method == "POST" && request.path == "/echo" {
    std.http_server.text(200, request.body)
  } else {
    std.http_server.not_found()
  }
}

fn main() {
  std.http_server.serve("127.0.0.1", 8080, handle)
}

Use serve for a straightforward service. It accepts connections and applies the handler to each request.

Test a handler

Create a detached request to exercise a handler directly:

fn test_echo() {
  let request = std.http_server.detached_request("POST", "/echo", "", "moon")
  let response = handle(request)

  std.testing.assert_eq(response.status, 200)
  std.testing.assert_eq(response.body, "moon")
}

This keeps route and response tests fast. Use a real local connection only for behavior that depends on streaming, disconnects, or HTTP framing.

Read request data

For small JSON or form requests, request.body is the simplest option. Put a reasonable body limit on public services.

For larger inputs:

Validate content type and size before doing expensive work, and stop reading as soon as the application knows it will reject the request.

Stream a response

Use respond_stream when the response should not be held in memory all at once. Write bounded chunks and finish the response explicitly:

let stream = std.http_server.respond_stream(request, 200, headers)
std.http_server.write_stream(stream, first_chunk)
std.http_server.write_stream(stream, second_chunk)
std.http_server.finish_stream(stream)

serve_file handles files and range requests. Prefer it over reading a large file into a string first.

Own the service loop

Use listen and accept when the application needs to decide how each connection is scheduled:

async fn serve_one(request: std.http_server.Request) -> int {
  let response = handle(request)
  std.http_server.respond(request, response)
  0
}

async fn main() {
  let listener = std.http_server.listen("127.0.0.1", 8080)
  while true {
    let request = std.http_server.accept(listener)
    spawn(serve_one(request))
  }
}

Close the listener during shutdown so a waiting accept can finish. Add timeouts and request-size limits that match the service rather than relying on unbounded reads.

TLS and routing

TLS-enabled moon installations provide serve_tls and listen_tls. A reverse proxy is also a good place to terminate TLS when it already owns certificates and deployment routing.

For larger services, the moon_router package adds path parameters, middleware, typed body binding, response helpers, and a ready-made serve loop. The lower-level std.http_server API remains useful when the application wants full control.

A served request runs in its own arena, so whatever a handler allocates while answering is released when the response is done. See Memory for what that means for values a handler wants to keep.

Browse every request, response, listener, and streaming function in the std.http_server API reference.