Current section
Files
Jump to
Current section
Files
src/nhttp_handler.erl
-module(nhttp_handler).
-moduledoc """
Handler behaviour for nhttp server.
Implement this behaviour to handle HTTP requests. Your handler module
is called synchronously for each request.
> #### HTTP/2 handler concurrency {: .info}
>
> On HTTP/2, `handle_request/2` runs in a per-stream worker process
> (`nhttp_stream_worker`), so a slow handler does not block other
> multiplexed streams on the same connection. This means that, on a
> single HTTP/2 connection, multiple workers may be executing handler
> callbacks concurrently. Each worker receives a snapshot of the
> connection's `handler_state`. The connection updates its own copy
> from each worker's `NewState` in arrival order. Concurrent streams
> therefore observe a last-write-wins view of `handler_state`. Do not
> use `handler_state` as the canonical source of truth for state that
> must be coordinated across requests on the same connection. Use
> ETS, gproc, or `persistent_term` for that.
>
> HTTP/1.1 keeps the synchronous in-process dispatch (no multiplexing),
> so its `handler_state` semantics are unchanged.
## Example Handler (Simple)
```erlang
-module(my_handler).
-behaviour(nhttp_handler).
-export([init/1, handle_request/2]).
init(_Args) ->
{ok, #{}}.
handle_request(#{method := get, path := <<"/">>}, State) ->
{reply, nhttp_resp:ok(<<"Hello, World!">>), State};
handle_request(_Req, State) ->
{reply, nhttp_resp:not_found(), State}.
```
## Lifecycle and `terminate/2`
`terminate/2` is **connection-scoped on every protocol**, not
request-scoped. On HTTP/1.1 a connection serves one request at a time,
so the boundaries coincide. On HTTP/2 and HTTP/3 a single connection
multiplexes many requests through per-stream workers, and `terminate/2`
fires once when the whole connection ends, not after each request.
This has practical consequences:
- **Resources opened inside `handle_request/2` or `handle_request_body/3`
must be released before that callback returns** (or, for streaming
responses, before the producer fun returns).
`nhttp_stream:producer/3` has `try/after` semantics for exactly this.
- **Do not rely on `terminate/2` to close per-request files, DB cursors,
or external sockets** opened during request handling on H2/H3. Those
resources will leak across the rest of the connection's lifetime.
- `terminate/2` is the right place for connection-scoped cleanup
(deregistration, connection-wide state shutdown), not per-request
cleanup.
`init/1` runs once per connection process, symmetric with `terminate/2`.
## Streaming responses
For streaming responses, return `{stream, Spec, State}` where `Spec` is
built with `nhttp_stream:producer/3` (push-based fun in a dedicated
worker). See `nhttp_stream` for examples.
## Streaming request bodies
To consume a request body chunk-by-chunk instead of buffering, return
`{accept_body, BodyState, State}` from `handle_request/2` and implement
`handle_request_body/3`. The connection forwards each `body_event/0`
(`{data, _}` | `{fin, Trailers}` | `{abort, Reason}`) to your callback,
threading `BodyState` between calls. Returning `{accept_body, _, _}`
again continues consuming. Any other tag (`reply`, `stream`, `upgrade`,
`abort`) terminates the body phase and proceeds to the response.
The connection applies end-to-end backpressure: TCP receive on HTTP/1.1,
WINDOW_UPDATE on HTTP/2, and QUIC stream flow control on HTTP/3 are all
gated on handler acknowledgement of the previous chunk. The configured
`max_body_size` remains a hard upper bound regardless of handler
willingness.
## Example Handler (WebSocket, async)
The WebSocket surface is split across four optional callbacks:
- `handle_ws_open/2` fires once after the upgrade response is on the wire.
Use it to register the session for external pushes (pubsub, gproc, ...).
- `handle_ws_frame/3` receives `text` and `binary` data frames. PING/PONG
frames are handled automatically. Opt in via `ws_runtime_opts` to also
observe them. CLOSE frames are NOT delivered here. See `handle_ws_closed/3`.
- `handle_ws_info/3` receives arbitrary Erlang messages, just like a
`gen_server` `handle_info/2` callback.
- `handle_ws_closed/3` fires once at session termination with an
RFC-grounded reason (`{peer | local | fail, Code, Reason}`,
`{transport, _}`, `{h2_reset | h3_reset, ErrorCode}`, `timeout`,
`shutdown`, `{handler_stop, _}`, `{handler_crash, _}`).
External processes push frames via `nhttp_ws:send/2,3` and
`nhttp_ws:send_async/2`. See `nhttp_ws` for the full session API.
```erlang
-module(ws_handler).
-behaviour(nhttp_handler).
-export([init/1, handle_request/2,
handle_ws_open/2, handle_ws_frame/3,
handle_ws_info/3, handle_ws_closed/3]).
init(_Args) -> {ok, #{}}.
handle_request(#{path := <<"/ws">>}, State) ->
{upgrade, websocket, State};
handle_request(_Req, State) ->
{reply, nhttp_resp:not_found(), State}.
handle_ws_open(Session, _State) ->
chat_room:join(Session),
{ok, #{session => Session}}.
handle_ws_frame({text, Data}, _Session, State) ->
{reply, {text, Data}, State}.
handle_ws_info({chat, From, Msg}, _Session, State) ->
{reply, {text, format(From, Msg)}, State}.
handle_ws_closed(_Reason, Session, _State) ->
chat_room:leave(Session),
ok.
```
See also: `nhttp_resp`, `nhttp_ws`.
""".
%%%-----------------------------------------------------------------------------
%% API EXPORTS
%%%-----------------------------------------------------------------------------
-export([
safe_handle_request/3,
safe_handle_request_body/4,
safe_handle_ws_closed/4,
safe_handle_ws_frame/4,
safe_handle_ws_info/4,
safe_handle_ws_open/3,
safe_init/2,
safe_terminate/3
]).
%%%-----------------------------------------------------------------------------
%% TYPES
%%%-----------------------------------------------------------------------------
-export_type([
abort_reason/0,
body_event/0,
handler_exception/0,
request_result/1,
ws_close_reason/0,
ws_result/1,
ws_runtime_opts/0,
ws_session_opts/0
]).
-type abort_reason() :: term().
-type body_event() ::
{data, binary()}
| {fin, nhttp_lib:headers()}
| {abort, abort_reason()}.
-type request_result(State) ::
{reply, Response :: nhttp_lib:response(), NewState :: State}
| {stream, Spec :: nhttp_stream:spec(), NewState :: State}
| {accept_body, BodyState :: term(), NewState :: State}
| {upgrade, websocket, NewState :: State}
| {upgrade, websocket, SessionOpts :: ws_session_opts(), NewState :: State}
| {abort, Reason :: abort_reason(), NewState :: State}.
-type handler_exception() ::
{nhttp_handler_exception, Class :: error | exit | throw, Reason :: term()}.
-type ws_close_reason() ::
{peer, Code :: nhttp_ws:close_code(), Reason :: binary()}
| {local, Code :: nhttp_ws:close_code(), Reason :: binary()}
| {fail, Code :: nhttp_ws:close_code(), Reason :: binary()}
| {transport, Cause :: term()}
| {h2_reset, ErrorCode :: non_neg_integer() | atom()}
| {h3_reset, ErrorCode :: non_neg_integer() | atom()}
| timeout
| shutdown
| {handler_stop, Reason :: term()}
| {handler_crash, {Class :: atom(), Reason :: term()}}.
-type ws_result(State) ::
{ok, NewState :: State}
| {reply, nhttp_ws:ws_message() | [nhttp_ws:ws_message()], NewState :: State}
| {close, Code :: nhttp_ws:close_code(), Reason :: binary(), NewState :: State}
| {stop, Reason :: term(), NewState :: State}.
-type ws_runtime_opts() :: nhttp_ws:ws_runtime_opts().
-type ws_session_opts() :: nhttp_ws:ws_session_opts().
%%%-----------------------------------------------------------------------------
%% BEHAVIOUR CALLBACKS
%%%-----------------------------------------------------------------------------
-callback handle_request(Request :: nhttp_lib:request(), State :: term()) ->
request_result(term()).
-callback handle_request_body(
Event :: body_event(),
BodyState :: term(),
State :: term()
) -> request_result(term()).
-callback handle_ws_open(Session :: nhttp_ws:session(), State :: term()) ->
{ok, SessionState :: term()}
| {ok, SessionState :: term(), Opts :: ws_runtime_opts()}
| {close, Code :: nhttp_ws:close_code(), Reason :: binary()}.
-callback handle_ws_frame(
Frame :: nhttp_ws:ws_frame(),
Session :: nhttp_ws:session(),
State :: term()
) -> ws_result(term()).
-callback handle_ws_info(
Info :: term(),
Session :: nhttp_ws:session(),
State :: term()
) -> ws_result(term()).
-callback handle_ws_closed(
Reason :: ws_close_reason(),
Session :: nhttp_ws:session(),
State :: term()
) -> ok | {ok, NewState :: term()}.
-callback init(Args :: term()) ->
{ok, State :: term()}
| {error, Reason :: term()}.
-callback terminate(Reason :: term(), State :: term()) -> ok.
-optional_callbacks([
handle_request_body/3,
handle_ws_closed/3,
handle_ws_frame/3,
handle_ws_info/3,
handle_ws_open/2,
terminate/2
]).
%%%-----------------------------------------------------------------------------
%% API
%%%-----------------------------------------------------------------------------
-doc false.
-spec safe_handle_request(module(), nhttp_lib:request(), term()) ->
request_result(term()) | handler_exception().
safe_handle_request(Handler, Request, State) ->
apply_safe(fun() -> Handler:handle_request(Request, State) end).
-doc false.
-spec safe_handle_request_body(module(), body_event(), term(), term()) ->
request_result(term()) | handler_exception().
safe_handle_request_body(Handler, Event, BodyState, State) ->
apply_safe(fun() -> Handler:handle_request_body(Event, BodyState, State) end).
-doc false.
-spec safe_handle_ws_closed(module(), ws_close_reason(), nhttp_ws:session(), term()) ->
ok | {ok, term()} | handler_exception().
safe_handle_ws_closed(Handler, Reason, Session, State) ->
apply_safe(fun() -> Handler:handle_ws_closed(Reason, Session, State) end).
-doc false.
-spec safe_handle_ws_frame(module(), nhttp_ws:ws_frame(), nhttp_ws:session(), term()) ->
ws_result(term()) | handler_exception().
safe_handle_ws_frame(Handler, Frame, Session, State) ->
apply_safe(fun() -> Handler:handle_ws_frame(Frame, Session, State) end).
-doc false.
-spec safe_handle_ws_info(module(), term(), nhttp_ws:session(), term()) ->
ws_result(term()) | handler_exception().
safe_handle_ws_info(Handler, Info, Session, State) ->
apply_safe(fun() -> Handler:handle_ws_info(Info, Session, State) end).
-doc false.
-spec safe_handle_ws_open(module(), nhttp_ws:session(), term()) ->
{ok, term()}
| {ok, term(), ws_runtime_opts()}
| {close, nhttp_ws:close_code(), binary()}
| handler_exception().
safe_handle_ws_open(Handler, Session, State) ->
apply_safe(fun() -> Handler:handle_ws_open(Session, State) end).
-doc false.
-spec safe_init(module(), term()) ->
{ok, term()} | {error, term()} | handler_exception().
safe_init(Handler, Args) ->
apply_safe(fun() -> Handler:init(Args) end).
-doc false.
-spec safe_terminate(module(), term(), term()) -> ok | handler_exception().
safe_terminate(Handler, Reason, State) ->
apply_safe(fun() -> ok = Handler:terminate(Reason, State) end).
%%%-----------------------------------------------------------------------------
%% INTERNAL FUNCTIONS
%%%-----------------------------------------------------------------------------
-spec apply_safe(fun(() -> Result)) -> Result | handler_exception().
apply_safe(Fun) ->
try Fun() of
Result -> Result
catch
Class:Err -> {nhttp_handler_exception, Class, Err}
end.