The response model
pub record Response {
status: Int,
headers: List (String, String),
body: ResponseBody,
}
pub type ResponseBody =
| Buffered String
| BufferedBytes BitString
| Streamed (Unit -> Unit needs {Chunked})You can build a Response directly, but the constructors cover the
common cases:
pub fun text : Int -> String -> Response # Content-Type: text/plain
pub fun bytes : Int -> List (String, String) -> BitString -> Response # no default Content-Type
pub fun stream : Int -> List (String, String) -> (Unit -> Unit needs {Chunked}) -> Responsetext defaults Content-Type: text/plain; bytes does not, because
binary payloads vary (image/png, application/octet-stream, …)
and guessing would be worse than forcing the caller to be explicit.
Streaming
stream takes a producer function that runs under the Chunked
effect. Each write_chunk! is sent as a single Transfer-Encoding
chunk; the terminating zero-chunk is emitted when the producer
returns.
fun handle : Request -> Response
handle _ = stream 200 [("Content-Type", "text/plain")] (fun () -> {
write_chunk! (BitString.from_string "alpha")
write_chunk! (BitString.from_string "beta")
})The library strips any user-supplied Content-Length and sets
Transfer-Encoding: chunked automatically on streamed responses
(mixing the two is a smuggling vector). Zero-length writes are
elided. If a write fails mid-stream a SendFailed SendChunk event
fires and the producer's continuation is dropped — your handler is
not resumed, the connection is closed.
Chunked is only in scope inside the stream producer. You cannot
call write_chunk! from your top-level handler; the body has to be
Streamed for it to be available.
Header sanitization
When a response is sent:
- Header names that contain non-
tcharbytes (CTLs, whitespace,:, high-bit bytes, …) are dropped silently to defend against user-controlled name smuggling. - CR and LF bytes in header values are replaced with space to prevent response splitting. The header still goes out, just neutralised.
Date and Server are injected automatically unless you've set them
yourself (case-insensitively). Set Config.server_name = "" to omit
Server entirely.
Pre-built error responses
For when you need to short-circuit:
pub fun bad_request # 400, Connection: close
pub fun request_timeout # 408, Connection: close
pub fun payload_too_large # 413, Connection: close
pub fun expectation_failed # 417, Connection: close
pub fun version_not_supported # 505, Connection: closeThese are values, not functions. The parser uses the same set when it rejects a request; you'd typically reach for them only if you're running a custom connection loop (see Recipe B).
status_text : Int -> String covers the common reason phrases and
returns "Unknown" for anything not in the table. TODO: the table is
small; if you need full coverage you'll want to wrap this.