1
1
# HTTP/2 Handler
2
2
3
3
Included in this folder is a complete ` ThousandIsland.Handler ` based implementation of HTTP/2 as
4
- defined in [ RFC 9113] ( https://datatracker.ietf.org/doc/rfc9113 ) .
4
+ defined in [ RFC 9110] ( https://datatracker.ietf.org/doc/rfc9110 ) & [ RFC
5
+ 9113] ( https://datatracker.ietf.org/doc/rfc9113 )
5
6
6
7
## Process model
7
8
@@ -10,24 +11,31 @@ Within a Bandit server, an HTTP/2 connection is modeled as a set of processes:
10
11
* 1 process per connection, a ` Bandit.HTTP2.Handler ` module implementing the
11
12
` ThousandIsland.Handler ` behaviour, and;
12
13
* 1 process per stream (i.e.: per HTTP request) within the connection, implemented as
13
- a ` Bandit.HTTP2.StreamTask ` Task
14
+ a ` Bandit.HTTP2.StreamProcess ` process
15
+
16
+ Each of these processes model the majority of their state via a
17
+ ` Bandit.HTTP2.Connection ` & ` Bandit.HTTP2.Stream ` struct, respectively.
14
18
15
19
The lifetimes of these processes correspond to their role; a connection process lives for as long
16
20
as a client is connected, and a stream process lives only as long as is required to process
17
- a single stream request within a connection.
21
+ a single stream request within a connection.
18
22
19
23
Connection processes are the 'root' of each connection's process group, and are supervised by
20
24
Thousand Island in the same manner that ` ThousandIsland.Handler ` processes are usually supervised
21
25
(see the [ project README] ( https://github.com/mtrudel/thousand_island ) for details).
22
26
23
- Stream processes are not supervised by design. The connection process starts new stream processes as required, and does so
24
- once a complete header block for a new stream has been received. It starts stream processes via
25
- a standard ` start_link ` call, and manages the termination of the resultant linked stream processes
26
- by handling ` {:EXIT,...} ` messages as described in the Elixir documentation. This approach is
27
- aligned with the realities of the HTTP/2 model, insofar as if a connection process terminates
28
- there is no reason to keep its constituent stream processes around, and if a stream process dies
29
- the connection should be able to handle this without itself terminating. It also means that our
30
- process model is very lightweight - there is no extra supervision overhead present because no such
27
+ Stream processes are not supervised by design. The connection process starts new
28
+ stream processes as required, via a standard ` start_link `
29
+ call, and manages the termination of the resultant linked stream processes by
30
+ handling ` {:EXIT,...} ` messages as described in the Elixir documentation. Each
31
+ stream process stays alive long enough to fully model an HTTP/2 stream,
32
+ beginning its life in the ` :init ` state and ending it in the ` :closed ` state (or
33
+ else by a stream or connection error being raised). This approach is aligned
34
+ with the realities of the HTTP/2 model, insofar as if a connection process
35
+ terminates there is no reason to keep its constituent stream processes around,
36
+ and if a stream process dies the connection should be able to handle this
37
+ without itself terminating. It also means that our process model is very
38
+ lightweight - there is no extra supervision overhead present because no such
31
39
supervision is required for the system to function in the desired way.
32
40
33
41
## Reading client data
@@ -40,13 +48,15 @@ looks like the following:
40
48
2 . Frames are parsed from these bytes by calling the ` Bandit.HTTP2.Frame.deserialize/2 `
41
49
function. If successful, the parsed frame(s) are returned. We retain any unparsed bytes in
42
50
a buffer in order to attempt parsing them upon receipt of subsequent data from the client
43
- 3 . Parsed frames are passed into the ` Bandit.HTTP2.Connection ` module along with a struct of
44
- same module. Frames are applied against this struct in a vaguely FSM-like manner, using pattern
45
- matching within the ` Bandit.HTTP2.Connection.handle_frame/3 ` function. Any side-effects of
46
- received frames are applied in these functions, and an updated connection struct is returned to
47
- represent the updated connection state. These side-effects can take the form of starting stream
48
- tasks, conveying data to running stream tasks, responding to the client with various frames, or
49
- any number of other actions
51
+ 3 . Parsed frames are passed into the ` Bandit.HTTP2.Connection ` module along with a struct of
52
+ same module. Frames are processed via the ` Bandit.HTTP2.Connection.handle_frame/3 ` function.
53
+ Connection-level frames are handled within the ` Bandit.HTTP2.Connection `
54
+ struct, and stream-level frames are passed along to the corresponding stream
55
+ process, which is wholly responsible for managing all aspects of a stream's
56
+ state (which is tracked via the ` Bandit.HTTP2.Stream ` struct). The one
57
+ exception to this is the handling of frames sent to streams which have
58
+ already been closed (and whose corresponding processes have thus terminated).
59
+ Any such frames are discarded without effect.
50
60
4 . This process is repeated every time we receive data from the client until the
51
61
` Bandit.HTTP2.Connection ` module indicates that the connection should be closed, either
52
62
normally or due to error. Note that frame deserialization may end up returning a connection
@@ -58,19 +68,15 @@ looks like the following:
58
68
59
69
## Processing requests
60
70
61
- The details of a particular stream are contained within a ` Bandit.HTTP2.Stream ` struct
62
- (as well as a ` Bandit.HTTP2.StreamTask ` process in the case of active streams). The
63
- ` Bandit.HTTP2.StreamCollection ` module manages a collection of streams, allowing for the memory
64
- efficient management of complete & yet unborn streams alongside active ones.
65
-
66
- Once a complete header block has been read, a ` Bandit.HTTP2.StreamTask ` is started to manage the
67
- actual calling of the configured ` Plug ` module for this server, using the ` Bandit.HTTP2.Adapter `
68
- module as the implementation of the ` Plug.Conn.Adapter ` behaviour. This adapter uses a simple
69
- ` receive ` pattern to listen for messages sent to it from the connection process, a pattern chosen
70
- because it allows for easy provision of the blocking-style API required by the ` Plug.Conn.Adapter `
71
- behaviour. Functions in the ` Bandit.HTTP2.Adapter ` behaviour which write data to the client use
72
- ` GenServer ` calls to the ` Bandit.HTTP2.Handler ` module in order to pass data to the connection
73
- process.
71
+ The state of a particular stream are contained within a ` Bandit.HTTP2.Stream `
72
+ struct, maintained within a ` Bandit.HTTP2.StreamProcess ` process. As part of the
73
+ stream's lifecycle, the server's configured Plug is called, with an instance of
74
+ the ` Bandit.HTTP2.Adapter ` struct being used to interface with the Plug. There
75
+ is a separation of concerns between the aspect of HTTP semantics managed by
76
+ ` Bandit.HTTP2.Adapter ` (roughly, those concerns laid out in
77
+ [ RFC9110] ( https://datatracker.ietf.org/doc/html/rfc9110 ) ) and the more
78
+ transport-specific HTTP/2 concerns managed by ` Bandit.HTTP2.Stream ` (roughly the
79
+ concerns specified in [ RFC9113] ( https://datatracker.ietf.org/doc/html/rfc9113 ) ).
74
80
75
81
# Testing
76
82
0 commit comments