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¶
- primitive AlreadyResponded
- primitive BodyTooLarge
- primitive CONNECT
- class ChunkSendToken
- primitive ChunkedNotSupported
- primitive ContentNegotiation
- type ContentNegotiationResult
- primitive CookiePrefixViolation
- primitive DELETE
- primitive DefaultIdleTimeout
- primitive GET
- primitive HEAD
- primitive HTTP10
- primitive HTTP11
- class HTTPServer
- trait HTTPServerActor
- trait HTTPServerLifecycleEventReceiver
- class Header
- class Headers
- primitive InvalidChunk
- primitive InvalidContentLength
- primitive InvalidCookieDomain
- primitive InvalidCookieName
- primitive InvalidCookiePath
- primitive InvalidCookieValue
- primitive InvalidURI
- primitive InvalidVersion
- type MakeMaxRequestsPerConnection
- primitive MalformedHeaders
- type MaxRequestsPerConnection
- class MediaType
- interface Method
- primitive Methods
- primitive NoAcceptableType
- primitive OPTIONS
- 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
- type Version