A minimal, low-level HTTP client for Erlang.
HTTP/1.1 and HTTP/2 support. Pure Erlang. Zero dependencies.
Fast. Small API. Proper HTTP/1.1 and HTTP/2 support. Works with both protocols transparently.
Built to replace httpc with better performance and cleaner code.
156 tests covering RFC 7540 (HTTP/2) and RFC 7541 (HPACK). All frame types, stream states, flow control, priority handling, HPACK compression, and error conditions.
Tested against h2-client-test-harness. 100% pass rate.
%% HTTP/1.1
{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
%% Collect response in active mode (default)
receive
Msg ->
case gen_http:stream(Conn2, Msg) of
{ok, Conn3, [{status, Ref, 200}, {headers, Ref, Headers}, {data, Ref, Body}, {done, Ref}]} ->
io:format("Body: ~s~n", [Body])
end
end.
%% HTTP/2 (automatic via ALPN)
{ok, Conn} = gen_http:connect(https, "google.com", 443),
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>),
%% Same API, different protocolAdd to your rebar.config:
{deps, [
{gen_http, {git, "https://github.com/codeadict/gen_http.git", {branch, "main"}}}
]}.{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
%% Active mode - receive messages
receive Msg ->
{ok, Conn3, Responses} = gen_http:stream(Conn2, Msg),
io:format("Responses: ~p~n", [Responses])
end.{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
Headers = [{<<"content-type">>, <<"application/json">>}],
Body = <<"{\"hello\": \"world\"}">>,
{ok, Conn2, Ref} = gen_http:request(Conn, <<"POST">>, <<"/post">>, Headers, Body).%% ALPN automatically negotiates HTTP/2 if available
{ok, Conn} = gen_http:connect(https, "www.google.com", 443),
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>).{ok, Conn} = gen_http:connect(http, "httpbin.org", 80, #{mode => passive}),
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
%% Blocking receive
{ok, Conn3, Responses} = gen_http:recv(Conn2, 0, 5000),
io:format("Responses: ~p~n", [Responses]).{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
%% First request
{ok, Conn2, Ref1} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
%% ... handle response ...
%% Second request on same connection
{ok, Conn3, Ref2} = gen_http:request(Conn2, <<"GET">>, <<"/headers">>, [], <<>>),
%% ... handle response ...
{ok, _} = gen_http:close(Conn3).case gen_http:connect(http, "example.com", 80) of
{ok, Conn} ->
case gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>) of
{ok, Conn2, Ref} ->
handle_success(Conn2, Ref);
{error, Conn2, Reason} ->
%% Structured errors: {transport_error, _}, {protocol_error, _}, {application_error, _}
case gen_http:is_retriable_error(Reason) of
true -> retry_request();
false -> handle_permanent_error(Reason)
end
end;
{error, Reason} ->
io:format("Connection failed: ~p~n", [Reason])
end.SSL/H2/ALPN tests need nghttpx (from nghttp2). Install on macOS with brew install nghttp2. Suites that need it skip gracefully if it's not installed.
# Run all tests
rebar3 test
# Run specific test types
rebar3 eunit # Unit tests
rebar3 ct # Integration tests
rebar3 proper # Property-based tests
# HTTP/2 compliance tests (excluded by default, requires separate Docker harness)
rebar3 ct --suite=h2_compliance_SUITEEarly development. API may change.
- Mint - HTTP client for Elixir
- httpcore - Minimal HTTP client for Python
- gun - HTTP client for Erlang
Apache 2.0