roadrunner_handler behaviour (roadrunner v0.1.0)
View SourceBehaviour for handling parsed HTTP requests.
Implementations receive the parsed request map and return a
{Response, Req2} pair — the Response selects what the conn does
on the wire, and Req2 is the (possibly mutated) request threaded
back to the conn. Always returning Req2 lets the conn drain
unread bodies in body_buffering => manual mode, lets response
middlewares observe and rewrite, and matches cowboy's idiom of
threading Req through the entire request lifecycle.
Response is one of:
{StatusCode, Headers, Body}— buffered response, encoded and sent in one shot.{stream, StatusCode, Headers, StreamFun}— chunked streaming. The connection emits status + headers (withTransfer-Encoding: chunkedauto-prepended) and callsStreamFun(Send)whereSend(Data, nofin | fin | {fin, Trailers})writes one chunk;finalso writes the size-0 terminator and{fin, Trailers}writes the terminator followed by the given trailer headers (RFC 9112 §7.1.2). Trailer names should be advertised in the response'sTrailerheader.{sendfile, StatusCode, Headers, {Filename, Offset, Length}}— zero-copy file body. The connection emits status + headers verbatim (the handler is responsible forContent-LengthandContent-Type), then dispatchesfile:sendfile/5for plain TCP (kernel-space copy) or a chunked read+send loop for TLS (where the kernel sendfile path can't see plaintext). Used byroadrunner_staticso large assets don't get copied through the Erlang heap.{loop, StatusCode, Headers, State}— message-driven streaming. The connection emits status + headers, then enters a receive loop in the conn process. Each Erlang message is dispatched through the optionalhandle_info/3callback, which can callPush(Data)to emit a chunk. Returning{stop, _}writes the size-0 terminator and closes. Useful for SSE/long-poll endpoints that subscribe to a pubsub topic inhandle/1and forward messages to the wire.{websocket, Module, State}— upgrade to aroadrunner_ws_handler.
If the handler did not call roadrunner_req:read_body/1,2, just thread
the original Req back. Idiomatic shape:
handle(Req) ->
{{200, [], ~"hello"}, Req}.
handle(Req) ->
{ok, Body, Req2} = roadrunner_req:read_body(Req),
{{200, [], Body}, Req2}.
Summary
Types
The push callback handed to a {loop, _, _, State} handler via
handle_info/3. Each call writes one chunk to the wire.
Handler response shape returned alongside the (mutated) request map.
What handle/1 returns: a pair of the handler's response/0 and
the (possibly mutated) request map. Threading Req2 back lets the
conn drain unread bodies in manual-buffering mode and response
middlewares observe / rewrite.
The chunk-writing callback handed to a stream_fun/0. Each call
emits one chunk on the wire
The {Filename, Offset, Length} triple for a {sendfile, _, _, _}
response. Bytes [Offset, Offset+Length) of the file are emitted
verbatim — the handler is responsible for setting Content-Length
and Content-Type headers on the response.
The stream callback for {stream, _, _, Fun} responses. The
framework calls Fun(Send) with a send_fun/0; the fun emits
chunks via Send and returns when the stream is complete.
Callbacks
Invoked once per parsed request. Receives the request map and
returns a {Response, Req2} pair where Response is one of the
shapes listed in the moduledoc (buffered, stream, sendfile, loop,
websocket) and Req2 is the (possibly mutated) request map threaded
back to the framework. Always return Req2 so the conn can drain
unread bodies in manual-buffering mode and response middlewares can
observe / rewrite.
Optional, only fired for {loop, _, _, State} responses. The
framework dispatches every non-OTP Erlang message delivered to the
conn (or h2 worker) process through this callback. Push(Data)
writes one chunk to the wire. Return {ok, NewState} to keep
looping or {stop, NewState} to emit the size-0 terminator and
close. Handlers that don't export this callback can't use
{loop, ...} responses.
Types
The push callback handed to a {loop, _, _, State} handler via
handle_info/3. Each call writes one chunk to the wire.
-type response() :: {StatusCode :: roadrunner_http:status(), roadrunner_http:headers(), Body :: iodata()} | {stream, StatusCode :: roadrunner_http:status(), roadrunner_http:headers(), stream_fun()} | {loop, StatusCode :: roadrunner_http:status(), roadrunner_http:headers(), State :: term()} | {sendfile, StatusCode :: roadrunner_http:status(), roadrunner_http:headers(), sendfile_spec()} | {websocket, Module :: module(), State :: term()}.
Handler response shape returned alongside the (mutated) request map.
Response header names MUST be ASCII lowercase. HTTP/2 requires this on the
wire per RFC 9113 §8.1.2 (clients reject responses with uppercase names);
the HTTP/1.1 path emits names verbatim, so the requirement is uniform
across protocols. Framework helpers (roadrunner_resp:*,
roadrunner_compress, the auto-injected ~"date" header) all emit
lowercase names; handler-supplied tuples must follow suit.
-type result() :: {response(), roadrunner_req:request()}.
What handle/1 returns: a pair of the handler's response/0 and
the (possibly mutated) request map. Threading Req2 back lets the
conn drain unread bodies in manual-buffering mode and response
middlewares observe / rewrite.
-type send_fun() :: fun((iodata(), nofin | fin | {fin, roadrunner_http:headers()}) -> ok | {error, term()}).
The chunk-writing callback handed to a stream_fun/0. Each call
emits one chunk on the wire:
Send(Data, nofin)— writeDataand expect more.Send(Data, fin)— writeDatathen the size-0 terminator.Send(Data, {fin, Trailers})— writeData, the terminator, and serialized trailer headers (RFC 9112 §7.1.2).
Returns ok on success or {error, Reason} if the wire write
failed (peer close, kernel error, etc.).
-type sendfile_spec() :: {Filename :: file:filename_all(), Offset :: non_neg_integer(), Length :: non_neg_integer()}.
The {Filename, Offset, Length} triple for a {sendfile, _, _, _}
response. Bytes [Offset, Offset+Length) of the file are emitted
verbatim — the handler is responsible for setting Content-Length
and Content-Type headers on the response.
The stream callback for {stream, _, _, Fun} responses. The
framework calls Fun(Send) with a send_fun/0; the fun emits
chunks via Send and returns when the stream is complete.
Callbacks
-callback handle(Request :: roadrunner_req:request()) -> result().
Invoked once per parsed request. Receives the request map and
returns a {Response, Req2} pair where Response is one of the
shapes listed in the moduledoc (buffered, stream, sendfile, loop,
websocket) and Req2 is the (possibly mutated) request map threaded
back to the framework. Always return Req2 so the conn can drain
unread bodies in manual-buffering mode and response middlewares can
observe / rewrite.
-callback handle_info(Info :: term(), Push :: push_fun(), State :: term()) -> {ok, NewState :: term()} | {stop, NewState :: term()}.
Optional, only fired for {loop, _, _, State} responses. The
framework dispatches every non-OTP Erlang message delivered to the
conn (or h2 worker) process through this callback. Push(Data)
writes one chunk to the wire. Return {ok, NewState} to keep
looping or {stop, NewState} to emit the size-0 terminator and
close. Handlers that don't export this callback can't use
{loop, ...} responses.