Skip to main content

src/nhttp_lib.erl

-module(nhttp_lib).

-moduledoc """
Core HTTP types for the nhttp library.

This module is the umbrella for the type vocabulary used across the
nhttp HTTP framework. Types defined elsewhere are re-exported here as
thin aliases so consumers only ever import from one upstream module.

## Core Types

- `method/0` - HTTP request methods (GET, POST, etc.)
- `version/0` - HTTP version (`http1_0` | `http1_1` | `http2` | `http3`)
- `scheme/0` - URI / wire scheme (`http` | `https` | `ws` | `wss`)
- `authority/0` - Authority component (`host[:port]`)
- `peer/0` - Remote socket address
- `header_name/0` / `header_value/0` - Header field name/value (binary)
- `headers/0` - HTTP headers as a list of name/value pairs
- `status/0` - HTTP response status codes (100-599)
- `stream_id/0` - Transport-level stream identifier (HTTP/2, HTTP/3, QPACK)
- `fin/0` - End-of-stream marker shared by HTTP/2 and HTTP/3
- `role/0` - Connection role (client or server)
- `error_code/0` - Open peer error code: an RFC-defined atom or the raw integer
- `action/0` - Caller-executed I/O action emitted by H2 / H3 codecs
- `error/0` - Unified error shape across H2 and H3
- `event_common/0` - Shape-shared events emitted by both H2 and H3 codecs
- `event_h2/0` - Alias for `t:nhttp_h2:event/0`
- `event_h3/0` - Alias for `t:nhttp_h3:event/0`
- `event/0` - Cross-protocol union; prefer `event_h2/0` or `event_h3/0`
- `request/0` - Canonical HTTP request shape across H1, H2, and H3
- `response/0` - Canonical HTTP response shape across H1, H2, and H3

The `version` field on `request/0` and `response/0` records the HTTP
version (`http1_0` | `http1_1` | `http2` | `http3`) and is filled by
every server parser. The `scheme` field is `http` or `https` on the
wire; `ws`/`wss` only appear when a client preserved the URI scheme as
parsed from a URL. WebSocket upgrades ride on `http`/`https` with
`Upgrade: websocket` (H1) or `:protocol = websocket` (H2/H3 Extended
CONNECT, RFC 8441 §4 / RFC 9220 §3).

The `connect_protocol` field on `request/0` carries the RFC 8441 / 9220
`:protocol` pseudo-header sent on Extended CONNECT requests
(`<<"websocket">>` for the WS upgrade case). It is independent of the
wire `version` field above and is only present when the peer sent
`:protocol`.

The `body` field is for the convenience case where the whole payload is
already in memory. The `streaming` atom signals that the body is being
fed chunk-by-chunk through the streaming-body callback (see
`nhttp_h1:parse_request_headers/2` and `parse_request_body/2`).

The `trailers` field is the symmetric convenience for trailing header
fields (RFC 9110 §6.5) when sent alongside an in-memory body. For
streaming, emit trailers separately after the body. H1 emits trailer
fields between `encode_last_chunk/0` and the closing CRLF; H2 / H3
emit them via a final `send_headers/4` call with `fin`.

## WebSocket umbrella re-exports

WS frame, message, and option types live in `nhttp_ws` / `nhttp_ws_frame`
and are re-exported here as aliases (`ws_message/0`, `ws_frame/0`,
`close_code/0`, `ws_send_opts/0`, `ws_session_opts/0`,
`ws_runtime_opts/0`, `ws_close_opts/0`).
""".

-export([
    decode_method/1,
    decode_scheme/1,
    encode_method/1,
    encode_scheme/1,
    request_to_pseudo_headers/1
]).

%%%-----------------------------------------------------------------------------
%% EXPORTS
%%%-----------------------------------------------------------------------------
-export_type([
    action/0,
    authority/0,
    error/0,
    error_code/0,
    event/0,
    event_common/0,
    event_h2/0,
    event_h3/0,
    fin/0,
    header_name/0,
    header_value/0,
    headers/0,
    method/0,
    peer/0,
    request/0,
    response/0,
    role/0,
    scheme/0,
    status/0,
    stream_id/0,
    version/0
]).

-export_type([
    close_code/0,
    ws_close_opts/0,
    ws_frame/0,
    ws_message/0,
    ws_runtime_opts/0,
    ws_send_opts/0,
    ws_session_opts/0
]).

%%%-----------------------------------------------------------------------------
%% TYPES
%%%-----------------------------------------------------------------------------
-type method() ::
    get
    | head
    | post
    | put
    | delete
    | connect
    | options
    | trace
    | patch
    | binary().

-type version() :: http1_0 | http1_1 | http2 | http3.

-type scheme() :: http | https | ws | wss.

-type authority() :: binary().

-type peer() :: {inet:ip_address(), inet:port_number()}.

-type header_name() :: binary().

-type header_value() :: binary().

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

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

-type stream_id() :: non_neg_integer().

-type fin() :: fin | nofin.

-type role() :: client | server.

-type error_code() :: atom() | non_neg_integer().

-type action() ::
    {send, stream_id() | connection, iodata()}
    | {send_fin, stream_id(), iodata()}
    | {close, error_code(), binary()}.

-type error() ::
    {connection_error, error_code(), binary()}
    | {stream_error, stream_id(), error_code(), binary()}
    | {protocol_violation, atom(), binary()}.

-type event_common() ::
    {request, stream_id(), request(), fin()}
    | {response, stream_id(), response(), fin()}
    | {data, stream_id(), binary(), fin()}
    | {trailers, stream_id(), headers()}
    | {stream_reset, stream_id(), error_code()}
    | {goaway, stream_id(), error_code(), binary()}.

-type event_h2() :: nhttp_h2:event().

-type event_h3() :: nhttp_h3:event().

-type event() :: event_h2() | event_h3().

-type request() :: #{
    method := method(),
    path := binary(),
    scheme := scheme(),
    authority := authority(),
    headers := headers(),
    peer => peer(),
    version => version(),
    body => iodata() | streaming,
    trailers => headers(),
    connect_protocol => binary()
}.

-type response() :: #{
    status := status(),
    version => version(),
    reason => binary(),
    headers := headers(),
    body => iodata(),
    trailers => headers()
}.

-type close_code() :: nhttp_ws:close_code().

-type ws_close_opts() :: nhttp_ws:ws_close_opts().

-type ws_frame() :: nhttp_ws:ws_frame().

-type ws_message() :: nhttp_ws:ws_message().

-type ws_runtime_opts() :: nhttp_ws:ws_runtime_opts().

-type ws_send_opts() :: nhttp_ws:ws_send_opts().

-type ws_session_opts() :: nhttp_ws:ws_session_opts().

%%%-----------------------------------------------------------------------------
%% CODECS
%%%-----------------------------------------------------------------------------
-doc """
Decode an HTTP method from its wire binary form. Known methods become
the corresponding atom (`get`, `post`, ...); unknown methods stay as
the input binary.
""".
-spec decode_method(binary()) -> method().
decode_method(<<"GET">>) -> get;
decode_method(<<"HEAD">>) -> head;
decode_method(<<"POST">>) -> post;
decode_method(<<"PUT">>) -> put;
decode_method(<<"DELETE">>) -> delete;
decode_method(<<"CONNECT">>) -> connect;
decode_method(<<"OPTIONS">>) -> options;
decode_method(<<"TRACE">>) -> trace;
decode_method(<<"PATCH">>) -> patch;
decode_method(M) when is_binary(M) -> M.

-doc """
Decode a scheme from its binary form. Strict: crashes with
`function_clause` on unknown values. The closed `t:scheme/0` union is
enforced here, so callers must validate untrusted input first (or wrap
the call in `try/catch`).
""".
-spec decode_scheme(binary()) -> scheme().
decode_scheme(<<"http">>) -> http;
decode_scheme(<<"https">>) -> https;
decode_scheme(<<"ws">>) -> ws;
decode_scheme(<<"wss">>) -> wss.

-doc "Encode an HTTP method to its wire binary form (`get` -> `<<\"GET\">>`).".
-spec encode_method(method()) -> binary().
encode_method(get) -> <<"GET">>;
encode_method(head) -> <<"HEAD">>;
encode_method(post) -> <<"POST">>;
encode_method(put) -> <<"PUT">>;
encode_method(delete) -> <<"DELETE">>;
encode_method(connect) -> <<"CONNECT">>;
encode_method(options) -> <<"OPTIONS">>;
encode_method(trace) -> <<"TRACE">>;
encode_method(patch) -> <<"PATCH">>;
encode_method(M) when is_binary(M) -> M.

-doc "Encode a URI / wire scheme to its binary form.".
-spec encode_scheme(scheme()) -> binary().
encode_scheme(http) -> <<"http">>;
encode_scheme(https) -> <<"https">>;
encode_scheme(ws) -> <<"ws">>;
encode_scheme(wss) -> <<"wss">>.

-doc """
Build the pseudo-header prefix plus regular headers for an HTTP/2 or
HTTP/3 request, ready to feed into `nhttp_h2:send_headers/4` or
`nhttp_h3:send_headers/4`.
Pseudo-header order is `:method`, `:scheme`, `:authority`, `:path`,
the conventional order; HTTP/2 (RFC 9113 §8.3) and HTTP/3 (RFC 9114
§4.3) require only that pseudo-headers precede regular fields.
The `Host` header is filtered out of the regular set because its value
already lives in `:authority` (which is required on `t:request/0`).
Other headers are passed through in their original order.
Extended CONNECT (RFC 8441 / 9220) is supported by leaving any
caller-supplied `{<<\":protocol\">>, _}` entry in the request's
`headers` list. The helper does not touch entries other than `Host`.
""".
-spec request_to_pseudo_headers(request()) -> headers().
request_to_pseudo_headers(
    #{method := M, path := P, scheme := S, authority := A, headers := H}
) ->
    [
        {<<":method">>, encode_method(M)},
        {<<":scheme">>, encode_scheme(S)},
        {<<":authority">>, A},
        {<<":path">>, P}
        | nhttp_headers:delete(<<"host">>, H)
    ].