Skip to content

Commit 3f89906

Browse files
committed
docs(http3): document low-level send_request/3 + concurrent stream handling
1 parent 1753824 commit 3f89906

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

guides/http3_guide.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,138 @@ Like HTTP/2, HTTP/3 multiplexes requests as streams on a single QUIC connection:
182182
└─────────────────────────────────────────────────────────────────┘
183183
```
184184

185+
## Low-Level Stream API
186+
187+
The high-level `hackney:get/post/...` functions cover the common case. For
188+
servers that send streamed responses, or when you want to drive several
189+
requests concurrently on the same QUIC connection, use the `hackney_quic`
190+
adapter directly.
191+
192+
### Connect
193+
194+
```erlang
195+
{ok, ConnRef} = hackney_quic:connect(<<"cloudflare.com">>, 443, #{}, self()).
196+
197+
receive
198+
{quic, ConnRef, {connected, _Info}} -> ok
199+
after 5000 ->
200+
error(connect_timeout)
201+
end.
202+
```
203+
204+
`hackney_quic:connect/4` registers the calling process as the owner of the
205+
connection. All events for the connection arrive as messages of the form
206+
`{quic, ConnRef, Event}`.
207+
208+
### Send a request
209+
210+
`send_request/3` opens a request stream and sends the HEADERS frame in one
211+
shot. Pass `Fin = true` when the request has no body, `false` if you will
212+
follow up with `send_data/4`:
213+
214+
```erlang
215+
Headers = [
216+
{<<":method">>, <<"GET">>},
217+
{<<":scheme">>, <<"https">>},
218+
{<<":authority">>, <<"cloudflare.com">>},
219+
{<<":path">>, <<"/cdn-cgi/trace">>}
220+
],
221+
{ok, StreamId} = hackney_quic:send_request(ConnRef, Headers, true).
222+
```
223+
224+
For requests with a body:
225+
226+
```erlang
227+
{ok, StreamId} = hackney_quic:send_request(ConnRef, Headers, false),
228+
ok = hackney_quic:send_data(ConnRef, StreamId, <<"chunk-1">>, false),
229+
ok = hackney_quic:send_data(ConnRef, StreamId, <<"chunk-2">>, true). %% Fin
230+
```
231+
232+
### Receive the response
233+
234+
The owner process receives a response as a sequence of events tagged with
235+
the `StreamId`:
236+
237+
```erlang
238+
recv(ConnRef, StreamId, Status, Headers, Body) ->
239+
receive
240+
{quic, ConnRef, {stream_headers, StreamId, RespHeaders, _Fin}} ->
241+
{<<":status">>, S} = lists:keyfind(<<":status">>, 1, RespHeaders),
242+
recv(ConnRef, StreamId, binary_to_integer(S),
243+
[H || {K, _} = H <- RespHeaders, K =/= <<":status">>],
244+
Body);
245+
{quic, ConnRef, {stream_data, StreamId, Chunk, true}} ->
246+
{ok, Status, Headers, <<Body/binary, Chunk/binary>>};
247+
{quic, ConnRef, {stream_data, StreamId, Chunk, false}} ->
248+
recv(ConnRef, StreamId, Status, Headers, <<Body/binary, Chunk/binary>>);
249+
{quic, ConnRef, {stream_reset, StreamId, ErrorCode}} ->
250+
{error, {stream_reset, ErrorCode}};
251+
{quic, ConnRef, {closed, Reason}} ->
252+
{error, {closed, Reason}}
253+
after 30000 ->
254+
{error, timeout}
255+
end.
256+
```
257+
258+
The `Fin = true` flag on a `stream_data` event marks end-of-stream. For
259+
header-only responses (HEAD, 204, 304) the adapter still emits a final
260+
`{stream_data, StreamId, <<>>, true}` so this loop terminates the same way.
261+
262+
### Concurrent streams on one connection
263+
264+
Since each request gets its own `StreamId`, you can have several in flight
265+
on the same QUIC connection and demultiplex on the StreamId in your receive:
266+
267+
```erlang
268+
{ok, S1} = hackney_quic:send_request(ConnRef, headers(<<"/">>), true),
269+
{ok, S2} = hackney_quic:send_request(ConnRef, headers(<<"/cdn-cgi/trace">>), true),
270+
{ok, S3} = hackney_quic:send_request(ConnRef, headers(<<"/robots.txt">>), true),
271+
272+
%% Collect responses as they complete; order is not guaranteed.
273+
collect(ConnRef, sets:from_list([S1, S2, S3]), #{}).
274+
275+
collect(_ConnRef, Pending, Acc) when map_size(Acc) =:= sets:size(Pending) ->
276+
Acc;
277+
collect(ConnRef, Pending, Acc) ->
278+
receive
279+
{quic, ConnRef, {stream_headers, SId, Hs, _}} ->
280+
collect(ConnRef, Pending, Acc#{SId => {Hs, <<>>}});
281+
{quic, ConnRef, {stream_data, SId, Chunk, true}} ->
282+
#{SId := {Hs, Body}} = Acc,
283+
collect(ConnRef, Pending, Acc#{SId => {Hs, <<Body/binary, Chunk/binary>>}});
284+
{quic, ConnRef, {stream_data, SId, Chunk, false}} ->
285+
#{SId := {Hs, Body}} = Acc,
286+
collect(ConnRef, Pending, Acc#{SId => {Hs, <<Body/binary, Chunk/binary>>}})
287+
end.
288+
```
289+
290+
### Cancel a stream
291+
292+
Use `reset_stream/3` to abort a single in-flight request without tearing
293+
down the connection:
294+
295+
```erlang
296+
ok = hackney_quic:reset_stream(ConnRef, StreamId, 16#0102). %% H3_REQUEST_CANCELLED
297+
```
298+
299+
### Close
300+
301+
```erlang
302+
hackney_quic:close(ConnRef, normal).
303+
```
304+
305+
### Event reference
306+
307+
| Event | Meaning |
308+
|----------------------------------------------------|---------------------------------------------------------|
309+
| `{connected, Info}` | QUIC + H3 handshake complete |
310+
| `{stream_headers, StreamId, Headers, Fin}` | Response headers (or trailers when `Fin = true`) |
311+
| `{stream_data, StreamId, Bin, Fin}` | Response body chunk; `Fin = true` ends the stream |
312+
| `{stream_reset, StreamId, ErrorCode}` | Peer reset the stream |
313+
| `{goaway, LastStreamId}` | Peer is shutting down; finish in-flight streams |
314+
| `{closed, Reason}` | Connection closed |
315+
| `{transport_error, Code, Reason}` | QUIC transport error |
316+
185317
## UDP Blocking and Fallback
186318

187319
Some networks block UDP traffic, which prevents HTTP/3 from working. Hackney handles this with negative caching:

0 commit comments

Comments
 (0)