nhttp_handler behaviour (nhttp v1.0.0)
View SourceHandler behaviour for nhttp server.
Implement this behaviour to handle HTTP requests. Your handler module is called synchronously for each request.
HTTP/2 handler concurrency
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)
-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/2orhandle_request_body/3must be released before that callback returns (or, for streaming responses, before the producer fun returns).nhttp_stream:producer/3hastry/aftersemantics for exactly this. - Do not rely on
terminate/2to 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/2is 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/2fires once after the upgrade response is on the wire. Use it to register the session for external pushes (pubsub, gproc, ...).handle_ws_frame/3receivestextandbinarydata frames. PING/PONG frames are handled automatically. Opt in viaws_runtime_optsto also observe them. CLOSE frames are NOT delivered here. Seehandle_ws_closed/3.handle_ws_info/3receives arbitrary Erlang messages, just like agen_serverhandle_info/2callback.handle_ws_closed/3fires 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.
-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.
Summary
Types
-type abort_reason() :: term().
-type body_event() :: {data, binary()} | {fin, nhttp_lib:headers()} | {abort, abort_reason()}.
-type handler_exception() :: {nhttp_handler_exception, Class :: error | exit | throw, Reason :: term()}.
-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 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().
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_closed(Reason :: ws_close_reason(), Session :: nhttp_ws:session(), State :: term()) -> ok | {ok, NewState :: term()}.
-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_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()}.