-module(roadrunner_handler).
-moduledoc """
Behaviour 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 (with `Transfer-Encoding: chunked`
auto-prepended) and calls `StreamFun(Send)` where
`Send(Data, nofin | fin | {fin, Trailers})` writes one chunk; `fin`
also 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's `Trailer`
header.
- `{sendfile, StatusCode, Headers, {Filename, Offset, Length}}` —
zero-copy file body. The connection emits status + headers
verbatim (the handler is responsible for `Content-Length` and
`Content-Type`), then dispatches `file:sendfile/5` for plain TCP
(kernel-space copy) or a chunked read+send loop for TLS (where
the kernel sendfile path can't see plaintext). Used by
`roadrunner_static` so 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
optional `handle_info/3` callback, which can call `Push(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 in `handle/1` and forward messages to the wire.
- `{websocket, Module, State}` — upgrade to a `roadrunner_ws_handler`.
If the handler did not call `roadrunner_req:read_body/1,2`, just thread
the original `Req` back. Idiomatic shape:
```erlang
handle(Req) ->
{{200, [], ~"hello"}, Req}.
handle(Req) ->
{ok, Body, Req2} = roadrunner_req:read_body(Req),
{{200, [], Body}, Req2}.
```
""".
-export_type([send_fun/0, stream_fun/0, push_fun/0, sendfile_spec/0, response/0, result/0]).
-doc """
The chunk-writing callback handed to a `t:stream_fun/0`. Each call
emits one chunk on the wire:
- `Send(Data, nofin)` — write `Data` and expect more.
- `Send(Data, fin)` — write `Data` then the size-0 terminator.
- `Send(Data, {fin, Trailers})` — write `Data`, 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 send_fun() ::
fun((iodata(), nofin | fin | {fin, roadrunner_http:headers()}) -> ok | {error, term()}).
-doc """
The stream callback for `{stream, _, _, Fun}` responses. The
framework calls `Fun(Send)` with a `t:send_fun/0`; the fun emits
chunks via `Send` and returns when the stream is complete.
""".
-type stream_fun() :: fun((send_fun()) -> any()).
-doc """
The push callback handed to a `{loop, _, _, State}` handler via
`handle_info/3`. Each call writes one chunk to the wire.
""".
-type push_fun() :: fun((iodata()) -> ok | {error, term()}).
-doc """
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.
""".
-type sendfile_spec() :: {
Filename :: file:filename_all(),
Offset :: non_neg_integer(),
Length :: non_neg_integer()
}.
-doc """
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 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()}.
-doc """
What `handle/1` returns: a pair of the handler's `t: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 result() :: {response(), roadrunner_req:request()}.
-doc """
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(Request :: roadrunner_req:request()) -> result().
-doc """
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.
""".
-callback handle_info(Info :: term(), Push :: push_fun(), State :: term()) ->
{ok, NewState :: term()} | {stop, NewState :: term()}.
-optional_callbacks([handle_info/3]).