h1 (h1 v0.9.0)

View Source

HTTP/1.1 public API.

Mirrors the surface of h2 (HTTP/2) and quic_h3 (HTTP/3) so applications can swap protocols without rewriting call sites.

Client

  {ok, Conn} = h1:connect("example.com", 80, #{}).
  {ok, StreamId} = h1:request(Conn, <<"GET">>, <<"/">>,
                              [{<<"host">>, <<"example.com">>}]).
  receive
      {h1, Conn, {response, StreamId, Status, _Headers}} -> ok
  end.
  ok = h1:close(Conn).

Server

  {ok, S} = h1:start_server(8080, #{
      transport => tcp,
      handler => fun(Conn, Id, _Method, _Path, _Hs) ->
          h1:send_response(Conn, Id, 200, [{<<"content-length">>, <<"2">>}]),
          h1:send_data(Conn, Id, <<"ok">>, true)
      end}).

Summary

Functions

Server: reply 200 Connection Established to a classic HTTP/1.1 CONNECT (RFC 9110 §9.3.6, RFC 9112 §3.2.3 authority-form request-target) and take ownership of the raw socket. Mirror of accept_upgrade/3 for the 101 Switching Protocols case, but writes status 200 and injects no Connection/Upgrade headers so bytes after CRLF belong to the tunnel.

Server: reply 101 Switching Protocols to an upgrade request and take ownership of the raw socket. Injects the Connection: Upgrade and Upgrade: <proto> framing headers itself and strips any caller-supplied copies (case-insensitive), so the 101 carries exactly one of each. Pass only protocol-specific extras in ExtraHeaders (e.g. sec-websocket-accept).

Server: send 100 Continue to a client waiting on Expect.

Address and port of the connected peer. Works in both modes: the remote client on a server connection, the remote server on a client connection.

Toggle request pipelining on a client connection.

Send a request using h2-compatible pseudo-headers. The list may contain :method, :path, :authority; they're translated into the HTTP/1.1 request line + Host header.

Server: send a complete response (status, headers, and body) in a single socket write and end the stream. A Content-Length is added when the headers carry neither Content-Length nor Transfer-Encoding, so the body is sent fixed-length rather than chunked. Use this for fully-known bodies; use send_response/4 + send_data/4 for streaming.

As respond/5, with per-response options. early_response_drain overrides the listener's drain budget for this response only: pass a larger {MaxBytes, MaxMs} to a known-large upload endpoint, or 0 to close immediately without draining.

Send a body chunk. With EndStream = true the stream is ended; if the request body was not fully received first (server side), h1 advertises Connection: close and drains the remaining inbound body before closing, as for respond/5.

Server: send an interim (1xx) response, e.g. 103 Early Hints, ahead of the final response. May be called several times per stream, until the final response headers are sent. 101 is rejected (use the Upgrade machinery), and so are HTTP/1.0 clients (RFC 9110 §15.2).

Serve an already-accepted connection: run the server loop on a socket someone else accepted. The caller has completed the TCP accept and, for TLS, the handshake and ALPN negotiation — h1 does not handshake again, so cert, key, verify and ssl_opts are ignored here. Use it to serve HTTP/1.1 and HTTP/2 on one TLS port: negotiate ALPN yourself, then hand http/1.1 sockets to this function.

Stop accepting new connections while continuing to serve the established ones (graceful drain). Synchronous. Call stop_server/1 afterwards to close the remaining connections.

Stop a server. Synchronous: closes the listen socket, the acceptor pool, and every accepted connection (kept-alive and in-flight included) before returning.

Client: send an Upgrade request and wait for 101 Switching Protocols.

Types

connect_opts/0

-type connect_opts() ::
          #{transport => tcp | ssl,
            ssl_opts => [ssl:tls_client_option()],
            connect_timeout => timeout(),
            timeout => timeout(),
            pipeline => boolean(),
            max_keepalive_requests => pos_integer(),
            max_header_block_size => pos_integer(),
            idle_timeout => timeout(),
            request_timeout => timeout()}.

connection/0

-type connection() :: pid().

early_response_drain/0

-type early_response_drain() :: 0 | {non_neg_integer() | infinity, non_neg_integer() | infinity}.

headers/0

-type headers() :: [{binary(), binary()}].

respond_opts/0

-type respond_opts() :: #{early_response_drain => early_response_drain()}.

server_opts/0

-type server_opts() ::
          #{transport => tcp | ssl,
            cert => binary() | string(),
            key => binary() | string(),
            cacerts => [binary()],
            verify => verify_none | verify_peer,
            ssl_opts => [ssl:tls_option()],
            ip => inet:ip_address(),
            inet6 => boolean(),
            handler :=
                fun((connection(), stream_id(), binary(), binary(), headers()) -> any()) | module(),
            acceptors => pos_integer(),
            handshake_timeout => timeout(),
            idle_timeout => timeout(),
            request_timeout => timeout(),
            early_response_drain => early_response_drain(),
            lingering_timeout => timeout(),
            pipeline => boolean(),
            max_keepalive_requests => pos_integer(),
            max_line_length => pos_integer(),
            max_request_line_size => pos_integer() | infinity,
            max_empty_lines => non_neg_integer(),
            max_header_name_size => pos_integer(),
            max_header_value_size => pos_integer(),
            max_headers => pos_integer(),
            max_header_block_size => pos_integer(),
            max_body_size => pos_integer() | infinity}.

server_ref/0

-type server_ref() :: {pid(), reference(), inet:port_number()}.

status/0

-type status() :: 100..599.

stream_id/0

-type stream_id() :: non_neg_integer().

Functions

accept_connect(Conn, StreamId, ExtraHeaders)

-spec accept_connect(connection(), stream_id(), headers()) ->
                        {ok, gen_tcp | ssl, term(), binary()} | {error, term()}.

Server: reply 200 Connection Established to a classic HTTP/1.1 CONNECT (RFC 9110 §9.3.6, RFC 9112 §3.2.3 authority-form request-target) and take ownership of the raw socket. Mirror of accept_upgrade/3 for the 101 Switching Protocols case, but writes status 200 and injects no Connection/Upgrade headers so bytes after CRLF belong to the tunnel.

accept_connect(Conn, StreamId, ExtraHeaders, Timeout)

-spec accept_connect(connection(), stream_id(), headers(), timeout()) ->
                        {ok, gen_tcp | ssl, term(), binary()} | {error, term()}.

accept_upgrade(Conn, StreamId, ExtraHeaders)

-spec accept_upgrade(connection(), stream_id(), headers()) -> {ok, term(), binary()} | {error, term()}.

Server: reply 101 Switching Protocols to an upgrade request and take ownership of the raw socket. Injects the Connection: Upgrade and Upgrade: <proto> framing headers itself and strips any caller-supplied copies (case-insensitive), so the 101 carries exactly one of each. Pass only protocol-specific extras in ExtraHeaders (e.g. sec-websocket-accept).

cancel(Conn, StreamId)

-spec cancel(connection(), stream_id()) -> ok | {error, term()}.

cancel(Conn, StreamId, Reason)

-spec cancel(connection(), stream_id(), term()) -> ok | {error, term()}.

cancel_stream(Conn, StreamId)

-spec cancel_stream(connection(), stream_id()) -> ok | {error, term()}.

cancel_stream(Conn, StreamId, Reason)

-spec cancel_stream(connection(), stream_id(), term()) -> ok | {error, term()}.

close(Conn)

-spec close(connection()) -> ok.

connect(Host, Port)

-spec connect(string() | binary(), inet:port_number()) -> {ok, connection()} | {error, term()}.

connect(Host, Port, Opts)

-spec connect(string() | binary(), inet:port_number(), connect_opts()) ->
                 {ok, connection()} | {error, term()}.

continue(Conn, StreamId)

-spec continue(connection(), stream_id()) -> ok | {error, term()}.

Server: send 100 Continue to a client waiting on Expect.

controlling_process(Conn, Pid)

-spec controlling_process(connection(), pid()) -> ok | {error, term()}.

get_peer_settings(Conn)

-spec get_peer_settings(connection()) -> map().

get_settings(Conn)

-spec get_settings(connection()) -> map().

goaway(Conn)

-spec goaway(connection()) -> ok | {error, term()}.

goaway(Conn, Reason)

-spec goaway(connection(), term()) -> ok | {error, term()}.

peername(Conn)

-spec peername(connection()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}.

Address and port of the connected peer. Works in both modes: the remote client on a server connection, the remote server on a client connection.

pipeline(Conn, Enabled)

-spec pipeline(connection(), boolean()) -> ok | {error, term()}.

Toggle request pipelining on a client connection.

request(Conn, Headers)

-spec request(connection(), headers()) -> {ok, stream_id()} | {error, term()}.

Send a request using h2-compatible pseudo-headers. The list may contain :method, :path, :authority; they're translated into the HTTP/1.1 request line + Host header.

request(Conn, Headers, Opts)

-spec request(connection(), headers(), map()) -> {ok, stream_id()} | {error, term()}.

request(Conn, Method, Path, Headers)

-spec request(connection(), binary(), binary(), headers()) -> {ok, stream_id()} | {error, term()}.

request(Conn, Method, Path, Headers, Body)

-spec request(connection(), binary(), binary(), headers(), binary()) ->
                 {ok, stream_id()} | {error, term()}.

respond(Conn, StreamId, Status, Headers, Body)

-spec respond(connection(), stream_id(), status(), headers(), iodata()) -> ok | {error, term()}.

Server: send a complete response (status, headers, and body) in a single socket write and end the stream. A Content-Length is added when the headers carry neither Content-Length nor Transfer-Encoding, so the body is sent fixed-length rather than chunked. Use this for fully-known bodies; use send_response/4 + send_data/4 for streaming.

If the request body has not been fully received when this is called (an early response, e.g. rejecting an oversized upload with 413), h1 adds Connection: close, sends the response, then drains and discards the rest of the inbound body before closing the socket. The response is delivered cleanly and the connection is not reused. The drain is bounded by the listener's early_response_drain budget (default {infinity, 30000}).

respond(Conn, StreamId, Status, Headers, Body, Opts)

-spec respond(connection(), stream_id(), status(), headers(), iodata(), respond_opts()) ->
                 ok | {error, term()}.

As respond/5, with per-response options. early_response_drain overrides the listener's drain budget for this response only: pass a larger {MaxBytes, MaxMs} to a known-large upload endpoint, or 0 to close immediately without draining.

send_data(Conn, StreamId, Data)

-spec send_data(connection(), stream_id(), binary()) -> ok | {error, term()}.

send_data(Conn, StreamId, Data, EndStream)

-spec send_data(connection(), stream_id(), binary(), boolean()) -> ok | {error, term()}.

Send a body chunk. With EndStream = true the stream is ended; if the request body was not fully received first (server side), h1 advertises Connection: close and drains the remaining inbound body before closing, as for respond/5.

send_informational(Conn, StreamId, Status, Headers)

-spec send_informational(connection(), stream_id(), 100..199, headers()) -> ok | {error, term()}.

Server: send an interim (1xx) response, e.g. 103 Early Hints, ahead of the final response. May be called several times per stream, until the final response headers are sent. 101 is rejected (use the Upgrade machinery), and so are HTTP/1.0 clients (RFC 9110 §15.2).

send_response(Conn, StreamId, Status, Headers)

-spec send_response(connection(), stream_id(), status(), headers()) -> ok | {error, term()}.

send_trailers(Conn, StreamId, Trailers)

-spec send_trailers(connection(), stream_id(), headers()) -> ok | {error, term()}.

serve_socket(Socket, Opts)

-spec serve_socket(ssl:sslsocket() | gen_tcp:socket(), server_opts()) -> {ok, pid()} | {error, term()}.

Serve an already-accepted connection: run the server loop on a socket someone else accepted. The caller has completed the TCP accept and, for TLS, the handshake and ALPN negotiation — h1 does not handshake again, so cert, key, verify and ssl_opts are ignored here. Use it to serve HTTP/1.1 and HTTP/2 on one TLS port: negotiate ALPN yourself, then hand http/1.1 sockets to this function.

Requires handler; every other option is the per-connection subset start_server/2 accepts (timeouts, parser limits, drain budget).

The socket must be passive ({active, false}): h1 arms it itself, and bytes an active socket already delivered to the caller's mailbox cannot be recovered.

On {ok, Pid} the returned process is linked to the caller and owns the socket: it is the socket's controlling process from that point on, and closes it when it exits (unless the socket was handed off by Upgrade or CONNECT). Killing the caller therefore closes the connection. These connections belong to no listener, so stop_server/1 does not close them.

  {ok, Sock} = ssl:handshake(Raw, 5000),
  {ok, <<"http/1.1">>} = ssl:negotiated_protocol(Sock),
  {ok, _Pid} = h1:serve_socket(Sock, #{handler => fun my_app:handle/5}).

server_port(_)

-spec server_port(server_ref()) -> inet:port_number().

set_stream_handler(Conn, StreamId, Pid)

-spec set_stream_handler(connection(), stream_id(), pid()) -> ok | {error, term()}.

set_stream_handler(Conn, StreamId, Pid, Opts)

-spec set_stream_handler(connection(), stream_id(), pid(), map()) -> ok | {error, term()}.

start_server(Port, Opts)

-spec start_server(inet:port_number(), server_opts()) -> {ok, server_ref()} | {error, term()}.

start_server(Name, Port, Opts)

-spec start_server(atom(), inet:port_number(), server_opts()) -> {ok, server_ref()} | {error, term()}.

stop_accepting(_)

-spec stop_accepting(server_ref()) -> ok.

Stop accepting new connections while continuing to serve the established ones (graceful drain). Synchronous. Call stop_server/1 afterwards to close the remaining connections.

stop_server(ServerRef)

-spec stop_server(server_ref()) -> ok.

Stop a server. Synchronous: closes the listen socket, the acceptor pool, and every accepted connection (kept-alive and in-flight included) before returning.

unset_stream_handler(Conn, StreamId)

-spec unset_stream_handler(connection(), stream_id()) -> ok.

upgrade(Conn, Protocol, Headers)

-spec upgrade(connection(), binary(), headers()) ->
                 {ok, stream_id(), term(), binary(), headers()} | {error, term()}.

Client: send an Upgrade request and wait for 101 Switching Protocols.

upgrade(Conn, Protocol, Headers, Timeout)

-spec upgrade(connection(), binary(), headers(), timeout()) ->
                 {ok, stream_id(), term(), binary(), headers()} | {error, term()}.

wait_connected(Conn)

-spec wait_connected(connection()) -> ok | {error, term()}.

wait_connected(Conn, Timeout)

-spec wait_connected(connection(), timeout()) -> ok | {error, term()}.