Skip to content

Commit 5b9077e

Browse files
committed
chore: Add docs compatible with OTP 27 and older
1 parent 301ccc6 commit 5b9077e

20 files changed

Lines changed: 1644 additions & 610 deletions

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Changelog
2+
3+
## 0.1.0
4+
5+
Initial release.
6+
7+
- HTTP/1.1 and HTTP/2 client support
8+
- Unified API across both protocols via `gen_http` module
9+
- ALPN protocol negotiation for HTTPS
10+
- Active and passive socket modes
11+
- Request pipelining (HTTP/1.1) and stream multiplexing (HTTP/2)
12+
- HPACK header compression with Huffman coding
13+
- Flow control (HTTP/2)
14+
- Structured error types with retry classification
15+
- Connection metadata via private key-value store
16+
- TLS certificate verification enabled by default

README.md

Lines changed: 19 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,8 @@ HTTP/1.1 and HTTP/2 support. Pure Erlang. Zero dependencies.
1414

1515
Fast. Small API. Proper HTTP/1.1 and HTTP/2 support. Works with both protocols transparently.
1616

17-
Built to replace `httpc` with better performance and cleaner code.
18-
19-
## HTTP/2 Compliance
20-
21-
**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.
22-
23-
Tested against [h2-client-test-harness](https://github.com/nomadlabsinc/h2-client-test-harness). **100% pass rate**.
24-
2517
## Quick Start
2618

27-
```erlang
28-
%% HTTP/1.1
29-
{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
30-
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
31-
32-
%% Collect response in active mode (default)
33-
receive
34-
Msg ->
35-
case gen_http:stream(Conn2, Msg) of
36-
{ok, Conn3, [{status, Ref, 200}, {headers, Ref, Headers}, {data, Ref, Body}, {done, Ref}]} ->
37-
io:format("Body: ~s~n", [Body])
38-
end
39-
end.
40-
41-
%% HTTP/2 (automatic via ALPN)
42-
{ok, Conn} = gen_http:connect(https, "google.com", 443),
43-
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>),
44-
%% Same API, different protocol
45-
```
46-
47-
## Installation
48-
4919
Add to your `rebar.config`:
5020

5121
```erlang
@@ -54,86 +24,44 @@ Add to your `rebar.config`:
5424
]}.
5525
```
5626

57-
## Examples
58-
59-
### Simple GET Request
27+
Send a request:
6028

6129
```erlang
6230
{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
6331
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
6432

65-
%% Active mode - receive messages
66-
receive Msg ->
67-
{ok, Conn3, Responses} = gen_http:stream(Conn2, Msg),
68-
io:format("Responses: ~p~n", [Responses])
33+
receive
34+
Msg ->
35+
case gen_http:stream(Conn2, Msg) of
36+
{ok, Conn3, [{status, Ref, 200}, {headers, Ref, Headers}, {data, Ref, Body}, {done, Ref}]} ->
37+
io:format("Body: ~s~n", [Body])
38+
end
6939
end.
7040
```
7141

72-
### POST with Body
73-
74-
```erlang
75-
{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
76-
77-
Headers = [{<<"content-type">>, <<"application/json">>}],
78-
Body = <<"{\"hello\": \"world\"}">>,
79-
80-
{ok, Conn2, Ref} = gen_http:request(Conn, <<"POST">>, <<"/post">>, Headers, Body).
81-
```
82-
83-
### HTTPS with HTTP/2
42+
HTTPS with automatic HTTP/2 negotiation:
8443

8544
```erlang
86-
%% ALPN automatically negotiates HTTP/2 if available
8745
{ok, Conn} = gen_http:connect(https, "www.google.com", 443),
8846
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>).
47+
%% Same API, different protocol
8948
```
9049

91-
### Passive Mode (Blocking)
92-
93-
```erlang
94-
{ok, Conn} = gen_http:connect(http, "httpbin.org", 80, #{mode => passive}),
95-
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
96-
97-
%% Blocking receive
98-
{ok, Conn3, Responses} = gen_http:recv(Conn2, 0, 5000),
99-
io:format("Responses: ~p~n", [Responses]).
100-
```
101-
102-
### Connection Reuse
50+
See the [Getting Started](docs/getting-started.md) guide for POST requests, passive mode, response collection loops, and more.
10351

104-
```erlang
105-
{ok, Conn} = gen_http:connect(http, "httpbin.org", 80),
106-
107-
%% First request
108-
{ok, Conn2, Ref1} = gen_http:request(Conn, <<"GET">>, <<"/get">>, [], <<>>),
109-
%% ... handle response ...
52+
## HTTP/2 Compliance
11053

111-
%% Second request on same connection
112-
{ok, Conn3, Ref2} = gen_http:request(Conn2, <<"GET">>, <<"/headers">>, [], <<>>),
113-
%% ... handle response ...
54+
**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.
11455

115-
{ok, _} = gen_http:close(Conn3).
116-
```
56+
Tested against [h2-client-test-harness](https://github.com/nomadlabsinc/h2-client-test-harness). **100% pass rate**.
11757

118-
### Error Handling
58+
## Documentation
11959

120-
```erlang
121-
case gen_http:connect(http, "example.com", 80) of
122-
{ok, Conn} ->
123-
case gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>) of
124-
{ok, Conn2, Ref} ->
125-
handle_success(Conn2, Ref);
126-
{error, Conn2, Reason} ->
127-
%% Structured errors: {transport_error, _}, {protocol_error, _}, {application_error, _}
128-
case gen_http:is_retriable_error(Reason) of
129-
true -> retry_request();
130-
false -> handle_permanent_error(Reason)
131-
end
132-
end;
133-
{error, Reason} ->
134-
io:format("Connection failed: ~p~n", [Reason])
135-
end.
136-
```
60+
- [Getting Started](docs/getting-started.md) -- installation, first request, collecting responses
61+
- [Architecture](docs/architecture.md) -- process-less design, module layout, protocol negotiation
62+
- [Active and Passive Modes](docs/active-and-passive-modes.md) -- choosing the right I/O model
63+
- [Error Handling](docs/error-handling.md) -- structured errors, retries, pattern matching
64+
- [SSL Certificates](docs/ssl-certificates.md) -- TLS config, custom CAs, ALPN
13765

13866
## Testing
13967

@@ -156,12 +84,6 @@ rebar3 ct --suite=h2_compliance_SUITE
15684

15785
Early development. API may change.
15886

159-
## Inspiration
160-
161-
- [Mint](https://github.com/elixir-mint/mint) - HTTP client for Elixir
162-
- [httpcore](https://github.com/encode/httpcore) - Minimal HTTP client for Python
163-
- [gun](https://github.com/ninenines/gun) - HTTP client for Erlang
164-
16587
## License
16688

16789
Apache 2.0

docs/active-and-passive-modes.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Active and Passive Modes
2+
3+
gen_http supports two socket modes, matching how OTP's `gen_tcp` and `ssl`
4+
modules work.
5+
6+
## Active Mode (Default)
7+
8+
In active mode, the socket sends data as Erlang messages to the
9+
controlling process. You receive them with `receive` and pass them
10+
to `gen_http:stream/2`:
11+
12+
```erlang
13+
{ok, Conn} = gen_http:connect(https, "example.com", 443),
14+
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>),
15+
16+
receive
17+
Msg ->
18+
case gen_http:stream(Conn2, Msg) of
19+
{ok, Conn3, Responses} ->
20+
%% handle Responses
21+
ok;
22+
{error, Conn3, Reason, _Partial} ->
23+
%% handle error
24+
ok;
25+
unknown ->
26+
%% not a socket message for this connection
27+
ok
28+
end
29+
end.
30+
```
31+
32+
gen_http uses `{active, once}` internally. After each message is consumed,
33+
it re-arms the socket for the next one. This gives you flow control --
34+
if your process falls behind, the socket won't flood it with data.
35+
36+
Active mode works well when your process needs to handle other messages
37+
alongside HTTP responses (timers, other sockets, gen_server calls).
38+
39+
## Passive Mode
40+
41+
In passive mode, you explicitly ask for data with `gen_http:recv/3`.
42+
The call blocks until data arrives or the timeout fires:
43+
44+
```erlang
45+
{ok, Conn} = gen_http:connect(https, "example.com", 443, #{mode => passive}),
46+
{ok, Conn2, Ref} = gen_http:request(Conn, <<"GET">>, <<"/">>, [], <<>>),
47+
48+
{ok, Conn3, Responses} = gen_http:recv(Conn2, 0, 5000).
49+
```
50+
51+
The second argument to `recv/3` is the byte count hint. Pass `0` to
52+
receive whatever data is available.
53+
54+
Passive mode is simpler for request-response patterns where you just
55+
want to fire a request and wait for the answer. It's also easier to
56+
reason about in sequential code.
57+
58+
## Switching Between Modes
59+
60+
You can switch modes on the fly with `gen_http:set_mode/2`:
61+
62+
```erlang
63+
%% Start in active mode
64+
{ok, Conn} = gen_http:connect(https, "example.com", 443),
65+
66+
%% Switch to passive for a blocking request
67+
{ok, Conn2} = gen_http:set_mode(Conn, passive),
68+
{ok, Conn3, Ref} = gen_http:request(Conn2, <<"GET">>, <<"/">>, [], <<>>),
69+
{ok, Conn4, Responses} = gen_http:recv(Conn3, 0, 5000),
70+
71+
%% Switch back to active
72+
{ok, Conn5} = gen_http:set_mode(Conn4, active).
73+
```
74+
75+
## Choosing a Mode
76+
77+
**Use active mode when:**
78+
- Your process handles multiple connections or message types
79+
- You want to interleave HTTP I/O with other work
80+
- You're building a connection pool or multiplexer
81+
- You need non-blocking operation
82+
83+
**Use passive mode when:**
84+
- You have a simple request-response workflow
85+
- You want blocking, sequential code
86+
- You're writing a script or one-off tool
87+
- You don't need to handle other messages while waiting
88+
89+
## Collecting a Full Response
90+
91+
Responses may arrive across multiple `stream/2` or `recv/3` calls,
92+
especially for large bodies. Here's a pattern that works for both modes:
93+
94+
```erlang
95+
collect_response(Conn, Ref, Mode) ->
96+
collect_response(Conn, Ref, Mode, #{}).
97+
98+
collect_response(Conn, Ref, active, Acc) ->
99+
receive
100+
Msg ->
101+
case gen_http:stream(Conn, Msg) of
102+
{ok, Conn2, Responses} ->
103+
case fold_responses(Responses, Ref, Acc) of
104+
{done, Result} -> {ok, Conn2, Result};
105+
{continue, Acc2} -> collect_response(Conn2, Ref, active, Acc2)
106+
end;
107+
{error, _Conn2, Reason, _} ->
108+
{error, Reason}
109+
end
110+
after 10000 ->
111+
{error, timeout}
112+
end;
113+
collect_response(Conn, Ref, passive, Acc) ->
114+
case gen_http:recv(Conn, 0, 5000) of
115+
{ok, Conn2, Responses} ->
116+
case fold_responses(Responses, Ref, Acc) of
117+
{done, Result} -> {ok, Conn2, Result};
118+
{continue, Acc2} -> collect_response(Conn2, Ref, passive, Acc2)
119+
end;
120+
{error, _Conn2, Reason} ->
121+
{error, Reason}
122+
end.
123+
124+
fold_responses([], _Ref, Acc) ->
125+
{continue, Acc};
126+
fold_responses([{status, Ref, Status} | Rest], Ref, Acc) ->
127+
fold_responses(Rest, Ref, Acc#{status => Status});
128+
fold_responses([{headers, Ref, Headers} | Rest], Ref, Acc) ->
129+
fold_responses(Rest, Ref, Acc#{headers => Headers});
130+
fold_responses([{data, Ref, Chunk} | Rest], Ref, Acc) ->
131+
Body = maps:get(body, Acc, <<>>),
132+
fold_responses(Rest, Ref, Acc#{body => <<Body/binary, Chunk/binary>>});
133+
fold_responses([{done, Ref} | _], Ref, Acc) ->
134+
{done, Acc}.
135+
```

0 commit comments

Comments
 (0)