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)
Stallion sends exactly the bytes you build. It does not inspect your response
and it does not rewrite it, so what you hand to respond() is what goes on the
wire. That keeps method-specific rules in your hands. The most common one to
get wrong is HEAD: a HEAD response must carry the same headers a GET would,
including Content-Length, but no body. Stallion will not strip the body for
you, so build the response without one. Set the headers, then add the body
chunk only when the method is not HEAD:
fun ref on_request_complete(request': stallion.Request val,
responder: stallion.Responder)
=>
let body: String val = "Hello, World!"
let response = stallion.ResponseBuilder(stallion.StatusOK)
.add_header("Content-Type", "text/plain")
.add_header("Content-Length", body.size().string())
.finish_headers()
if not (request'.method is stallion.HEAD) then
response.add_chunk(body)
end
responder.respond(response.build())
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
For one-shot timers (request processing deadlines, application-level timeouts),
use stallion.HTTPServer.set_timer(). Unlike idle timeout, this timer fires
unconditionally — I/O activity does not reset it. Only one timer can be active
per connection at a time. The typical pattern is a processing deadline: set a
timer, delegate work to another actor, and race the result against the deadline:
actor MyServer is stallion.HTTPServerActor
var _http: stallion.HTTPServer = stallion.HTTPServer.none()
let _database: Database tag
var _timer: (lori.TimerToken | None) = None
var _responder: (stallion.Responder | None) = None
// ... constructor ...
fun ref _http_connection(): stallion.HTTPServer => _http
fun ref on_request_complete(request': stallion.Request val,
responder: stallion.Responder)
=>
match lori.MakeTimerDuration(5_000)
| let d: lori.TimerDuration =>
match _http.set_timer(d)
| let t: lori.TimerToken =>
_timer = t
_responder = responder
_database.query(request', this)
| let err: lori.SetTimerError => None
end
end
be query_result(data: String val) =>
// Work completed before the deadline — cancel timer and respond
match (_timer, _responder)
| (let t: lori.TimerToken, let r: stallion.Responder) =>
_http.cancel_timer(t)
_timer = None
_responder = None
let response = stallion.ResponseBuilder(stallion.StatusOK)
.add_header("Content-Length", data.size().string())
.finish_headers()
.add_chunk(data)
.build()
r.respond(response)
end
fun ref on_timer(token: lori.TimerToken) =>
// Deadline expired — worker didn't finish in time
match (_timer, _responder)
| (let t: lori.TimerToken, let r: stallion.Responder) if t == token =>
_timer = None
_responder = None
let body: String val = "Request timed out"
let response = stallion.ResponseBuilder(stallion.StatusRequestTimeout)
.add_header("Content-Length", body.size().string())
.finish_headers()
.add_chunk(body)
.build()
r.respond(response)
end
Public Types¶
- primitive AlreadyResponded
- primitive BadHostHeader
- primitive BareCRLF
- primitive BodyTooLarge
- primitive CONNECT
- class ChunkSendToken
- primitive ChunkedNotSupported
- primitive ContentLengthWithTransferEncoding
- primitive ContentNegotiation
- type ContentNegotiationResult
- primitive CookiePrefixViolation
- primitive DELETE
- primitive DefaultIdleTimeout
- primitive ForbiddenTrailer
- primitive GET
- primitive HEAD
- primitive HTTP10
- primitive HTTP11
- class HTTPServer
- trait HTTPServerActor
- trait HTTPServerLifecycleEventReceiver
- class Header
- class Headers
- primitive InvalidChunk
- primitive InvalidChunkExtension
- primitive InvalidContentLength
- primitive InvalidCookieDomain
- primitive InvalidCookieName
- primitive InvalidCookiePath
- primitive InvalidCookieValue
- primitive InvalidFieldName
- primitive InvalidFieldValue
- primitive InvalidHostValue
- primitive InvalidRequestLine
- primitive InvalidTransferEncoding
- primitive InvalidURI
- primitive InvalidVersion
- type MakeMaxRequestsPerConnection
- type MaxRequestsPerConnection
- class MediaType
- interface Method
- primitive Methods
- primitive MismatchedHost
- primitive MissingConnectPort
- primitive NoAcceptableType
- primitive OPTIONS
- primitive ObsFold
- primitive PATCH
- primitive POST
- primitive PUT
- primitive ParseCookies
- type ParseError
- class Request
- class RequestCookie
- class RequestCookies
- class Responder
- interface ResponseBodyBuilder
- primitive ResponseBuilder
- interface ResponseHeadersBuilder
- type SameSite
- primitive SameSiteLax
- primitive SameSiteNone
- primitive SameSiteRequiresSecure
- primitive SameSiteStrict
- class ServerConfig
- class SetCookie
- type SetCookieBuildError
- class SetCookieBuilder
- type StartChunkedResponseResult
- interface Status
- primitive StatusAccepted
- primitive StatusBadGateway
- primitive StatusBadRequest
- primitive StatusConflict
- primitive StatusContinue
- primitive StatusCreated
- primitive StatusForbidden
- primitive StatusFound
- primitive StatusGatewayTimeout
- primitive StatusGone
- primitive StatusHTTPVersionNotSupported
- primitive StatusInternalServerError
- primitive StatusLengthRequired
- primitive StatusMethodNotAllowed
- primitive StatusMovedPermanently
- primitive StatusNoContent
- primitive StatusNotAcceptable
- primitive StatusNotFound
- primitive StatusNotImplemented
- primitive StatusNotModified
- primitive StatusOK
- primitive StatusPartialContent
- primitive StatusPayloadTooLarge
- primitive StatusPermanentRedirect
- primitive StatusRequestHeaderFieldsTooLarge
- primitive StatusRequestTimeout
- primitive StatusSeeOther
- primitive StatusServiceUnavailable
- primitive StatusSwitchingProtocols
- primitive StatusTemporaryRedirect
- primitive StatusTooManyRequests
- primitive StatusURITooLong
- primitive StatusUnauthorized
- primitive StatusUnprocessableEntity
- primitive StatusUnsupportedMediaType
- primitive StreamingStarted
- primitive TRACE
- primitive TooLarge
- primitive UnknownMethod
- primitive UnsupportedTransferEncoding
- primitive UserinfoInTarget
- type Version