Current section

Files

Jump to
erlang_h1 src h1_server.erl
Raw

src/h1_server.erl

%% Copyright (c) 2026 Benoit Chesneau.
%% SPDX-License-Identifier: Apache-2.0
%%
%% @doc HTTP/1.1 server connection loop.
%%
%% Each accepted connection spawns an h1_server process. It owns the
%% socket, performs the TLS handshake (in ssl mode), starts an
%% h1_connection in server mode, and dispatches request events to the
%% user handler.
-module(h1_server).
-export([init_accepted/6]).
-type transport() :: gen_tcp | ssl.
-spec init_accepted(pid(), term(), transport(), term(), map(), map()) -> ok.
init_accepted(_Parent, Socket, Transport, Handler, ConnOpts, ServerOpts) ->
receive
{h1_acceptor, socket_ready} ->
handle_accepted(Socket, Transport, Handler, ConnOpts, ServerOpts);
{h1_acceptor, transfer_failed} ->
ok
after 5000 ->
close(Transport, Socket)
end.
handle_accepted(Socket, ssl, Handler, ConnOpts, ServerOpts) ->
HandshakeTimeout = maps:get(handshake_timeout, ServerOpts, 30000),
case ssl:handshake(Socket, HandshakeTimeout) of
{ok, TlsSocket} ->
run_connection(TlsSocket, ssl, Handler, ConnOpts);
{error, _Reason} ->
_ = ssl:close(Socket),
ok
end;
handle_accepted(Socket, gen_tcp, Handler, ConnOpts, _ServerOpts) ->
run_connection(Socket, gen_tcp, Handler, ConnOpts).
run_connection(Socket, Transport, Handler, ConnOpts) ->
case h1_connection:start_link(server, Socket, self(), ConnOpts) of
{ok, Conn} ->
case transfer(Transport, Socket, Conn) of
ok ->
case h1_connection:activate(Conn) of
ok ->
connection_loop(Conn, Handler);
{error, _} ->
try h1_connection:close(Conn) catch _:_ -> ok end
end;
{error, _} ->
try h1_connection:close(Conn) catch _:_ -> ok end,
close(Transport, Socket)
end;
{error, _Reason} ->
close(Transport, Socket)
end.
transfer(gen_tcp, Socket, Pid) -> gen_tcp:controlling_process(Socket, Pid);
transfer(ssl, Socket, Pid) -> ssl:controlling_process(Socket, Pid).
close(gen_tcp, Sock) -> _ = gen_tcp:close(Sock), ok;
close(ssl, Sock) -> _ = ssl:close(Sock), ok.
%% ----------------------------------------------------------------------------
%% Event loop
%% ----------------------------------------------------------------------------
%% Each request runs in a dedicated handler process so body/trailers can
%% be delivered as `{h1_stream, StreamId, _}' messages. The connection
%% loop waits for the handler to finish before accepting the next
%% request — this keeps pipelined response bytes in order on the wire
%% (RFC 9112 §9.3).
connection_loop(Conn, Handler) ->
receive
{h1, Conn, {request, StreamId, Method, Path, Headers}} ->
{Pid, MRef} = start_handler(Conn, StreamId, Method, Path,
Headers, Handler),
pump(Conn, Handler, Pid, MRef, StreamId);
{h1, Conn, {upgrade, StreamId, _Proto, Method, Path, Headers}} ->
%% Hand the upgrade request to the user handler exactly as a
%% regular request — it can inspect `Upgrade:' in Headers
%% and call `h1:accept_upgrade/3' to switch protocols.
{Pid, MRef} = start_handler(Conn, StreamId, Method, Path,
Headers, Handler),
pump(Conn, Handler, Pid, MRef, StreamId);
{h1, Conn, {upgraded, _StreamId, _Proto, _Sock, _Buf}} ->
ok;
{h1, Conn, {upgraded, _StreamId, _Proto, _Sock, _Buf, _Hs}} ->
ok;
{h1, Conn, {goaway, _, _}} ->
ok;
{h1, Conn, {closed, _Reason}} ->
ok;
{'EXIT', Conn, _Reason} ->
ok;
_Other ->
connection_loop(Conn, Handler)
end.
pump(Conn, Handler, Pid, MRef, StreamId) ->
receive
{h1, Conn, {data, StreamId, Data, End}} ->
Pid ! {h1_stream, StreamId, {data, Data, End}},
pump(Conn, Handler, Pid, MRef, StreamId);
{h1, Conn, {trailers, StreamId, T}} ->
Pid ! {h1_stream, StreamId, {trailers, T}},
pump(Conn, Handler, Pid, MRef, StreamId);
{h1, Conn, {stream_reset, StreamId, R}} ->
Pid ! {h1_stream, StreamId, {stream_reset, R}},
pump(Conn, Handler, Pid, MRef, StreamId);
{'DOWN', MRef, process, Pid, _Reason} ->
connection_loop(Conn, Handler);
{h1, Conn, {closed, Reason}} ->
%% Connection gone mid-stream: unblock the handler (which may be
%% waiting for the next {h1_stream, _} body chunk) before exiting.
Pid ! {h1_stream, StreamId, {stream_reset, Reason}},
ok;
{'EXIT', Conn, Reason} ->
Pid ! {h1_stream, StreamId, {stream_reset, Reason}},
ok
end.
start_handler(Conn, StreamId, Method, Path, Headers, Handler) ->
spawn_monitor(fun() ->
try
invoke(Handler, Conn, StreamId, Method, Path, Headers)
catch
Class:Reason:Stack ->
error_logger:error_msg("h1 handler crashed: ~p:~p~n~p~n",
[Class, Reason, Stack]),
try
h1_connection:send_response(
Conn, StreamId, 500,
[{<<"content-length">>, <<"21">>},
{<<"content-type">>, <<"text/plain">>}]),
h1_connection:send_data(
Conn, StreamId, <<"Internal Server Error">>, true)
catch _:_ -> ok end
end
end).
invoke(Fun, Conn, StreamId, Method, Path, Headers) when is_function(Fun, 5) ->
Fun(Conn, StreamId, Method, Path, Headers);
invoke(Mod, Conn, StreamId, Method, Path, Headers) when is_atom(Mod) ->
Mod:handle_request(Conn, StreamId, Method, Path, Headers).