Skip to content

Package

HTTP server for Pony, built on lori.

A listener actor implements lori.TCPListenerActor and creates stallion.HTTPServerActor instances in _on_accept. Each connection actor owns a stallion.HTTPServer that handles HTTP parsing and response management, delivering HTTP events via stallion.HTTPServerLifecycleEventReceiver callbacks.

use stallion = "stallion"
use lori = "lori"

actor Main
  new create(env: Env) =>
    let auth = lori.TCPListenAuth(env.root)
    MyListener(auth, "localhost", "8080")

actor MyListener is lori.TCPListenerActor
  var _tcp_listener: lori.TCPListener = lori.TCPListener.none()
  let _server_auth: lori.TCPServerAuth
  let _config: stallion.ServerConfig

  new create(auth: lori.TCPListenAuth, host: String, port: String) =>
    _server_auth = lori.TCPServerAuth(auth)
    _config = stallion.ServerConfig(host, port)
    _tcp_listener = lori.TCPListener(auth, host, port, this)

  fun ref _listener(): lori.TCPListener => _tcp_listener

  fun ref _on_accept(fd: U32): lori.TCPConnectionActor =>
    MyServer(_server_auth, fd, _config)

actor MyServer is stallion.HTTPServerActor
  var _http: stallion.HTTPServer = stallion.HTTPServer.none()

  new create(auth: lori.TCPServerAuth, fd: U32,
    config: stallion.ServerConfig)
  =>
    _http = stallion.HTTPServer(auth, fd, this, config)

  fun ref _http_connection(): stallion.HTTPServer => _http

  fun ref on_request_complete(request': stallion.Request val,
    responder: stallion.Responder)
  =>
    let body: String val = "Hello!"
    let response = stallion.ResponseBuilder(stallion.StatusOK)
      .add_header("Content-Length", body.size().string())
      .finish_headers()
      .add_chunk(body)
      .build()
    responder.respond(response)

For streaming responses, use chunked transfer encoding. start_chunked_response() returns a stallion.StartChunkedResponseResult indicating success or the reason for failure. Each send_chunk() returns a stallion.ChunkSendToken — override on_chunk_sent() to drive flow-controlled delivery:

fun ref on_request_complete(request': stallion.Request val,
  responder: stallion.Responder)
=>
  match responder.start_chunked_response(stallion.StatusOK)
  | stallion.StreamingStarted =>
    let token = responder.send_chunk("chunk 1")
    // When on_chunk_sent(token) fires, send the next chunk...
    responder.send_chunk("chunk 2")
    responder.finish_response()
  | stallion.ChunkedNotSupported =>
    // HTTP/1.0 — fall back to a complete response
    responder.respond(fallback_response)
  | stallion.AlreadyResponded => None
  end

For HTTPS, use stallion.HTTPServer.ssl instead of stallion.HTTPServer. Store an SSLContext val in the listener and pass it through in _on_accept:

use stallion = "stallion"
use "files"
use "ssl/net"
use lori = "lori"

actor Main
  new create(env: Env) =>
    let sslctx = recover val
      SSLContext
        .> set_cert(
          FilePath(FileAuth(env.root), "cert.pem"),
          FilePath(FileAuth(env.root), "key.pem"))?
        .> set_client_verify(false)
        .> set_server_verify(false)
    end
    let auth = lori.TCPListenAuth(env.root)
    MyListener(auth, "localhost", "8443", sslctx)

actor MyListener is lori.TCPListenerActor
  var _tcp_listener: lori.TCPListener = lori.TCPListener.none()
  let _server_auth: lori.TCPServerAuth
  let _config: stallion.ServerConfig
  let _ssl_ctx: SSLContext val

  new create(auth: lori.TCPListenAuth, host: String, port: String,
    ssl_ctx: SSLContext val)
  =>
    _ssl_ctx = ssl_ctx
    _server_auth = lori.TCPServerAuth(auth)
    _config = stallion.ServerConfig(host, port)
    _tcp_listener = lori.TCPListener(auth, host, port, this)

  fun ref _listener(): lori.TCPListener => _tcp_listener

  fun ref _on_accept(fd: U32): lori.TCPConnectionActor =>
    MyServer(_server_auth, fd, _config, _ssl_ctx)

The actor explicitly chooses stallion.HTTPServer (plain HTTP) or stallion.HTTPServer.ssl (HTTPS) in its constructor. The MyServer actor in the HTTPS example would use stallion.HTTPServer.ssl(auth, ssl_ctx, fd, this, config) instead of stallion.HTTPServer(auth, fd, this, config).

Cookies are automatically parsed from Cookie request headers and available via request'.cookies. Use stallion.ParseCookies for direct parsing, and stallion.SetCookieBuilder to construct validated Set-Cookie response headers with secure defaults:

fun ref on_request_complete(request': stallion.Request val,
  responder: stallion.Responder)
=>
  // Read a cookie from the request
  let session = match request'.cookies.get("session")
  | let s: String val => s
  else "anonymous"
  end

  // Build a Set-Cookie header (defaults: Secure, HttpOnly, SameSite=Lax)
  match stallion.SetCookieBuilder("session", "new-token")
    .with_path("/")
    .with_max_age(3600)
    .build()
  | let sc: stallion.SetCookie val =>
    let body: String val = "Hello, " + session + "!"
    let response = stallion.ResponseBuilder(stallion.StatusOK)
      .add_header("Content-Length", body.size().string())
      .add_header("Set-Cookie", sc.header_value())
      .finish_headers()
      .add_chunk(body)
      .build()
    responder.respond(response)
  | let err: stallion.SetCookieBuildError =>
    // Handle validation error
    None
  end

For content negotiation, use stallion.ContentNegotiation to select a response content type based on the client's Accept header. This is opt-in — most endpoints serve a single content type, so automatic parsing would waste CPU. Call it only in handlers that support multiple formats:

fun ref on_request_complete(request': stallion.Request val,
  responder: stallion.Responder)
=>
  let supported = [as stallion.MediaType val:
    stallion.MediaType("application", "json")
    stallion.MediaType("text", "plain")
  ]
  match stallion.ContentNegotiation.from_request(request', supported)
  | let mt: stallion.MediaType val =>
    // Respond with the negotiated content type
    let body: String val = "Hello!"
    let response = stallion.ResponseBuilder(stallion.StatusOK)
      .add_header("Content-Type", mt.string())
      .add_header("Content-Length", body.size().string())
      .finish_headers()
      .add_chunk(body)
      .build()
    responder.respond(response)
  | stallion.NoAcceptableType =>
    // 406 Not Acceptable
    let body: String val = "Not Acceptable"
    let response = stallion.ResponseBuilder(
        stallion.StatusNotAcceptable)
      .add_header("Content-Length", body.size().string())
      .finish_headers()
      .add_chunk(body)
      .build()
    responder.respond(response)
  end

Public Types