Joe DeVivo / @joedevivo
That's good!
That's bad!
Well, sometimes. Be responsible
This specification describes an optimized expression of the semantics of the Hypertext Transfer Protocol (HTTP), referred to as HTTP version 2 (HTTP/2). HTTP/2 enables a more efficient use of network resources and a reduced perception of latency by introducing header field compression and allowing multiple concurrent exchanges on the same connection. It also introduces unsolicited push of representations from servers to clients.
This specification is an alternative to, but does not obsolete, the HTTP/1.1 message syntax. HTTP's existing semantics remain unchanged.
Google tried to fix this and pretty much proved it could work
2015 - RFC 7540
Uses TLS's "Next Protocol Negotiation"
Or TLS's "Application Layer Protocol Negotiation"
Uses "Upgrade: " header.
HTTP/2 servers respond with status code
101 Switching
HTTP is stateless
Stateless protocols are repetitive
Stateless protocols are repetitive
A whole RFC just for header compression!
Lookup table for common and recently used headers
+-------+--------------------+---------------+ | Index | Header Name | Header Value | +-------+--------------------+---------------+ | 1 | :authority | | | 2 | :method | GET | | 3 | :method | POST | | 4 | :path | / | | 5 | :path | /index.html | | 6 | :scheme | http | | 7 | :scheme | https | | 8 | :status | 200 | | 13 | :status | 404 | | 14 | :status | 500 | | 15 | accept-charset | | | 16 | accept-encoding | gzip, deflate | ... | 57 | transfer-encoding | | | 58 | user-agent | | | 59 | vary | | | 60 | via | | | 61 | www-authenticate | | +-------+--------------------+---------------+
[{1 , <<":authority">> , undefined},
{2 , <<":method">> , <<"GET">>},
{3 , <<":method">> , <<"POST">>},
{4 , <<":path">> , <<"/">>},
{5 , <<":path">> , <<"/index.html">>},
{6 , <<":scheme">> , <<"http">>},
{7 , <<":scheme">> , <<"https">>},
{8 , <<":status">> , <<"200">>},
{13 , <<":status">> , <<"404">>},
{14 , <<":status">> , <<"500">>},
{15 , <<"accept-charset">> , undefined},
{16 , <<"accept-encoding">> , <<"gzip, deflate">>},
...
{57 , <<"transfer-encoding">>, undefined},
{58 , <<"user-agent">> , undefined},
{59 , <<"vary">> , undefined},
{60 , <<"via">> , undefined},
{61 , <<"www-authenticate">> , undefined}]
Add your own!
Indexes 62+
Bounded by size in HTTP/2 connection settings as a security precaution
-type header_name() :: binary().
-type header_value():: binary().
-define(DYNAMIC_TABLE_MIN_INDEX, 62).
-record(dynamic_table, {
table = [] :: [{pos_integer(), header_name(), header_value()}],
max_size = 4096 :: pos_integer(),
size = 0 :: non_neg_integer()
}).
-type dynamic_table() :: #dynamic_table{}.
-spec encode([{binary(), binary()}], encode_context()) -> {binary(), encode_context()}.
-spec decode(binary(), decode_context()) -> {headers(), decode_context()}.
StaticTable = hpack:new_encode_context(),
{HeaderBin, StaticTable} = encode([{<<":status">>, <<"200">>}], StaticTable).
StaticTable = hpack:new_decode_context(),
{[{<<":status">>, <<"200">>}], StaticTable} = decode(HeaderBin, StaticTable).
StaticTable = hpack:new_encode_context(),
{HeaderBin, NewContext} = encode([{<<":status">>, <<"600">>}], StaticTable),
NewContext =/= StaticTable,
%% Second time we try and encode this header
{HeaderBin, NewContext} = encode([{<<":status">>, <<"600">>}], NewContext).
%% Order Matters!
Given two peers: X & Y, connected over C1
For a connection between two peers, Client and Server, ServerContext and ClientContext are both initialized with the values from the static table
encode(Headers, ClientContext) -> ClientContext', EncodedBin
decode(EncodedBin, ServerContext) -> ServerContext', Headers
ClientContext' == ServerContext'
+---------------+ +---------------+ |Peer X (Client)| |Peer Y (Server)| +-------------------+---------------+ +---------------+-------------------+ | | | | | +----------+ +-----------+ +--+-----------+--+ +-----------+ +----------+ | | |Plain Req | |Encode (A1)| | Encoded Request | |Decode (A2)| |Plain Req | | | | Headers |-->| Context |-->| Headers |-->| Context |-->| Headers | | | +----------+ +-----------+ +--+-----------+--+ +-----------+ +----------+ | | | Cloud | | | +----------+ +-----------+ +--+-----------+--+ +-----------+ +----------+ | | |Plain Resp| |Decode (B2)| |Encoded Response | |Encode (B1)| |Plain Resp| | | | Headers |<--| Context |<--| Headers |<--+- Context |<--| Headers | | | +----------+ +-----------+ +--+-----------+--+ +-----------+ +----------+ | | | | | | | | | +-----------------------------------+ +-----------------------------------+
+---------------+ +---------------+ |Peer X (Client)| |Peer Y (Server)| +-------------------+---------------+ +---------------+-------------------+ | | | | | +----------+ +-----------+ +--+-----------+--+ +-----------+ +----------+ | | |Plain Req | | | | Encoded Request | | | |Plain Req | | | |Headers #1|-->| |-->| Headers #1 |-->| |-->|Headers #1| | | +----------+ | | +--+-----------+--+ | | +----------+ | | +----------+ | | +--+-----------+--+ | | +----------+ | | |Plain Req | | | | Encoded Request | | | |Plain Req | | | |Headers #2|-->| |-->| Headers #2 |-->| |-->|Headers #2| | | +----------+ |Encode (A1)| +--+-----------+--+ |Decode (A2)| +----------+ | | +----------+ | Context | +--+-----------+--+ | Context | +----------+ | | |Plain Req | | | | Encoded Request | | | |Plain Req | | | |Headers #3|-->| |-->| Headers #3 |-->| |-->|Headers #3| | | +----------+ | | +--+-----------+--+ | | +----------+ | | +----------+ | | +--+-----------+--+ | | +----------+ | | |Plain Req | | | | Encoded Request | | | |Plain Req | | | |Headers #4|-->| |-->| Headers #4 |-->| |-->|Headers #4| | | +----------+ +-----------+ +--+-----------+--+ +-----------+ +----------+ | +-----------------------------------+ +-----------------------------------+
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 1 | Index (7+) | +---+---------------------------+
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | Index (6+) | +---+---+-----------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | 0 | +---+---+-----------------------+ | H | Name Length (7+) | +---+---------------------------+ | Name String (Length octets) | +---+---------------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
decode(<<>>, HeadersAcc, C) -> {HeadersAcc, C};
%% First bit is '1', so it's an 'Indexed Header Feild'
decode(<<2#1:1,_/bits>>=B, HeaderAcc, Context) ->
decode_indexed_header(B, HeaderAcc, Context);
%% First two bits are '01' so it's a 'Literal Header Field with Incremental Indexing'
decode(<<2#01:2,_/bits>>=B, HeaderAcc, Context) ->
decode_literal_header_with_indexing(B, HeaderAcc, Context);
%% First four bits are '0000' so it's a 'Literal Header Field without Indexing'
decode(<<2#0000:4,_/bits>>=B, HeaderAcc, Context) ->
decode_literal_header_without_indexing(B, HeaderAcc, Context);
%% First four bits are '0001' so it's a 'Literal Header Field never Indexed'
decode(<<2#0001:4,_/bits>>=B, HeaderAcc, Context) ->
decode_literal_header_never_indexed(B, HeaderAcc, Context);
%% First three bits are '001' so it's a 'Dynamic Table Size Update'
decode(<<2#001:3,_/bits>>=B, HeaderAcc, Context) ->
decode_dynamic_table_size_update(B, HeaderAcc, Context);
There are tons of cool examples in HPACK: Appendix C
They're so good that I turned them into EUnit tests
Headers1 = [
{<<":path">>, <<"/">>},
{<<"user-agent">>, <<"my cool browser">>},
{<<"x-custom-header">>, <<"some custom value">>}
],
HeaderContext1 = hpack:new_encode_context(),
{HeadersBin1, HeaderContext2} = hpack:encode(Headers1, HeaderContext1),
Headers2 = [
{<<":path">>, <<"/some_file.html">>},
{<<"user-agent">>, <<"my cool browser">>},
{<<"x-custom-header">>, <<"some custom value">>}
],
{HeadersBin2, HeaderContext3} = hpack:encode(Headers2, HeaderContext2),
Headers3 = [
{<<":path">>, <<"/some_file.html">>},
{<<"user-agent">>, <<"my cool browser">>},
{<<"x-custom-header">>, <<"new value">>}
],
{HeadersBin3, _HeaderContext4} = hpack:encode(Headers3, HeaderContext3),
Headers1 = [
{<<":path">>, <<"/">>},
{<<"user-agent">>, <<"my cool browser">>},
{<<"x-custom-header">>, <<"some custom value">>}
],
Header: :path: / Representation: Indexed Header Field Index: 4 Header: user-agent: my cool browser Representation: Literal Header Field with Incremental Indexing - Indexed Name Index: 58 Value: my cool browser Header: x-custom-header: some custom value Representation: Literal Header Field with Incremental Indexing - New Name Name: x-custom-header Value: some custom value
DynamicTable = [
{62,<<"x-custom-header">>,<<"some custom value">>},
{63,<<"user-agent">>, <<"my cool browser">>}
]
Headers2 = [
{<<":path">>, <<"/some_file.html">>},
{<<"user-agent">>, <<"my cool browser">>},
{<<"x-custom-header">>, <<"some custom value">>}
],
Header: :path: /some_file.html Representation: Literal Header Field with Incremental Indexing - Indexed Name Index: 4 Value: /some_file.html Header: user-agent: my cool browser Representation: Indexed Header Field Index: 64 Header: x-custom-header: some custom value Representation: Indexed Header Field Index: 63
[
{62,<<":path">>, <<"/some_file.html">>},
{63,<<"x-custom-header">>,<<"some custom value">>},
{64,<<"user-agent">>, <<"my cool browser">>}
]
Headers3 = [
{<<":path">>, <<"/some_file.html">>},
{<<"user-agent">>, <<"my cool browser">>},
{<<"x-custom-header">>, <<"new value">>}
],
Header: :path: /some_file.html Representation: Indexed Header Field Index: 62 Header: user-agent: my cool browser Representation: Indexed Header Field Index: 64 Header: x-custom-header: new value Representation: Literal Header Field with Incremental Indexing - Indexed Name Index: 63 Value: new value
[
{62,<<"x-custom-header">>,<<"new value">>},
{63,<<":path">>, <<"/some_file.html">>},
{64,<<"x-custom-header">>,<<"some custom value">>},
{65,<<"user-agent">>, <<"my cool browser">>}
]
There's one connection, streams are logical abstractions across it
They have unique identifiers
At most one Request / one Response per stream
client initiated stream ids are always odd
Stream 0 is the meta stream.
Frames sent to it apply to the entire connection
Some frame types (e.g. Data) can't be sent to stream 0.
A frame's stream identifier determines
which multiplexed stream it belongs to
A series of frames for each stream id is reconstructed on the other side, in the order they arrive
+---------------+ +---------------+ |Peer X (Client)| |Peer Y (Server)| +---------------+ +---------------+------------------------------+ | | | ---------+ +--+-----------+--+ +-----------+ +----------+ | | | Encoded Request | | | |Plain Req | | |-->| Headers #1 |------------->| |-->|Headers #1| | | +--+-----------+--+ | | +----------+ | | +--+-----------+--+ | | +----------+ | | | Encoded Request | | | | Bad Req | | |-->| Headers #2 |------\ | |-->|Headers #3| | code (A1)| +--+-----------+--+ ----\----->|Decode (A2)| +----------+ | Context | +--+-----------+--+ / \ | Context | +----------+ | | | Encoded Request |-/ \ | | |Bad Req | | |-->| Headers #3 | --->| |-->|Headers #2| | | +--+-----------+--+ | | +----------+ | | +--+-----------+--+ | | +----------+ | | | Encoded Request | | | |Plain Req | | |-->| Headers #4 |------------->| |-->|Headers #4| | ---------+ +--+-----------+--+ +-----------+ +----------+ | | | | ---------+ +--+-----------+--+ +-----------+ +----------+ | code (B2)| |Encoded Response | |Encode (B1)| |Plain Resp| | Context |<--| Headers |<-------------| Context |<--| Headers | | ---------+ +--+-----------+--+ +-----------+ +----------+ | ----------------+ +----------------------------------------------+
A connection has multiple streams each of which can accept a single request message, and send a single response message, each message consisting of multiple frames in order
There are rules in the spec about how many can be "active" at a time, and what types of frames can be received when
+--------+ send PP | | recv PP ,--------| idle |--------. / | | \ v +--------+ v +----------+ | +----------+ | | | send H / | | ,------| reserved | | recv H | reserved |------. | | (local) | | | (remote) | | | +----------+ v +----------+ | | | +--------+ | | | | recv ES | | send ES | | | send H | ,-------| open |-------. | recv H | | | / | | \ | | | v v +--------+ v v | | +----------+ | +----------+ | | | half | | | half | | | | closed | | send R / | closed | | | | (remote) | | recv R | (local) | | | +----------+ | +----------+ | | | | | | | | send ES / | recv ES / | | | | send R / v send R / | | | | recv R +--------+ recv R | | | send R / `----------->| |<-----------' send R / | | recv R | closed | recv R | `----------------------->| |<----------------------' +--------+
+-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+=============================================================+ | Frame Payload (0...) ... +---------------------------------------------------------------+
One of 10 frame types. Each has rules for payload sizes, payload content and which flags can be set.
A few control bits that have different uses for different frame types
A shoutout to pirates
Which multiplexed stream this frame is for
+-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+=============================================================+ | Frame Payload (0...) ... +---------------------------------------------------------------+
-spec read_binary_frame_header(binary()) -> {frame_header(), binary()}.
read_binary_frame_header(<<Length:24,Type:8,Flags:8,_R:1,StreamId:31,Rem/bits>>) ->
Header = #frame_header{
length = Length,
type = Type,
flags = Flags,
stream_id = StreamId
},
{Header, Rem}.
Negotiates overall connection settings
measuring round trip, also keeps the connection open
We're done here. Could be an error, could just be natual causes
Request and Response Headers start with these
HEADERS are the first frame of headers, but if they're too big to fit in one frame, the rest comes over in CONTINUATION frames. When a frame comes over with the END_HEADERS flag set, we know it's over
Request and Response Bodies are made of these
Sent to give up on a stream
+-------------------------+ |HEADERS | +-------------------------+ |CONTINUATION | +-------------------------+ |CONTINUATION END_HEADERS| +-------------------------+ |DATA | +-------------------------+ |DATA | +-------------------------+ |DATA END_STREAM | +-------------------------+
route_frame({#frame_header{length=L}, _},
S = #http2_connection_state{
connection=#connection_state{
recv_settings=#settings{max_frame_size=MFS}
}})
when L > MFS ->
go_away(?FRAME_SIZE_ERROR, S);
A Fauxnite State Machine
+--------+ send PP | | recv PP ,--------| idle |--------. / | | \ v +--------+ v +----------+ | +----------+ | | | send H / | | ,------| reserved | | recv H | reserved |------. | | (local) | | | (remote) | | | +----------+ v +----------+ | | | +--------+ | | | | recv ES | | send ES | | | send H | ,-------| open |-------. | recv H | | | / | | \ | | | v v +--------+ v v | | +----------+ | +----------+ | | | half | | | half | | | | closed | | send R / | closed | | | | (remote) | | recv R | (local) | | | +----------+ | +----------+ | | | | | | | | send ES / | recv ES / | | | | send R / v send R / | | | | recv R +--------+ recv R | | | send R / `----------->| |<-----------' send R / | | recv R | closed | recv R | `----------------------->| |<----------------------' +--------+
-record(stream_state, {
stream_id = undefined :: stream_id(),
state = idle :: stream_state_name()
}).
-spec recv_frame(frame(), {stream_state(), connection_state()}) ->
{stream_state(), connection_state()}.
-spec send_frame(frame(), {stream_state(), connection_state()}) ->
{stream_state(), connection_state()}.
route_frame(F={H=#frame_header{stream_id=StreamId}, _Payload},
S = #connection_state{
decode_context=DecodeContext,
recv_settings=#settings{initial_window_size=RecvWindowSize},
send_settings=#settings{initial_window_size=SendWindowSize},
streams=Streams,
content_handler = Handler
})
when H#frame_header.type == ?HEADERS,
?IS_FLAG(H#frame_header.flags, ?FLAG_END_HEADERS) ->
HeadersBin = http2_frame_headers:from_frames([F]),
{Headers, NewDecodeContext} = hpack:decode(HeadersBin, DecodeContext),
Stream = http2_stream:new(StreamId),
State Machine without a Process
-spec new(stream_id()) -> stream_state().
new(StreamId) ->
#stream_state{
stream_id=StreamId
}.
{Stream2, NextConnectionState} =
http2_stream:recv_frame(
F,
{Stream,
S#connection_state{
decode_context=NewDecodeContext
}}),
recv_frame(F={FH = #frame_header{
stream_id=StreamId,
type=?HEADERS
}, _Payload},
{State = #stream_state{state=idle},
ConnectionState})
when ?IS_FLAG(FH#frame_header.flags, ?FLAG_END_STREAM) ->
{State#stream_state{
stream_id = StreamId,
state = half_closed_remote,
incoming_frames = [F]
}, ConnectionState};
recv_frame(F={_FH = #frame_header{
stream_id=StreamId,
type=?HEADERS
}, _Payload},
{State = #stream_state{state=idle},
ConnectionState}) ->
{State#stream_state{
stream_id = StreamId,
state = open,
incoming_frames = [F]
}, ConnectionState};
{NewStreamState, NewConnectionState} =
Handler:handle(
NextConnectionState,
Headers,
Stream2),
{next_state, connected, NewConnectionState#connection_state{
streams = [{StreamId, NewStreamState}|Streams]
}};
Applied to both the Connection as a whole and Individual Streams
Each receiver advertises how many bytes it can receive
Won't send more than the receiver advertises
The window update frame is sent by the receiver to increase the number of bytes advertised. There is no decrement operation.
This is the only frame type that works on both the connection and stream level
Ext = filename:extension(File),
MimeType = case Ext of
".js" -> <<"text/javascript">>;
".html" -> <<"text/html">>;
".css" -> <<"text/css">>;
_ -> <<"unknown">>
end,
{ok, Data} = file:read_file(File),
ResponseHeaders = [
{<<":status">>, <<"200">>},
{<<"content-type">>, MimeType}
],
{HeaderFrame, NewContext} = http2_frame_headers:to_frame(StreamId, ResponseHeaders, EncodeContext),
DataFrames = http2_frame_data:to_frames(StreamId, Data, SS),
{NewContext, [HeaderFrame|DataFrames]};
%% This is a baller fold right here. Fauxnite State Machine at its finest.
lists:foldl(
fun(Frame, State) ->
http2_stream:send_frame(Frame, State)
end,
{Stream, NewConnectionState},
Frames).
send_frame(F={#frame_header{length=L,type=?DATA},_Data},
{StreamState = #stream_state{
send_window_size=SSWS},
ConnectionState = #connection_state{
send_window_size=CSWS}
})
when SSWS >= L, CSWS >= L ->
Transport:send(Socket, http2_frame:to_binary(F)),
{StreamState#stream_state{
send_window_size=SSWS-L
},
ConnectionState#connection_state{
send_window_size=CSWS-L
}
};
send_frame(F={#frame_header{
type=?DATA
},_Data},
{StreamState = #stream_state{
queued_frames = QF
},
ConnectionState = #connection_state{}}) ->
{StreamState#stream_state{
queued_frames=QF ++ [F] %% I know, I know queue:in
},
ConnectionState};
%% needs a WINDOW_UPDATE clause badly
recv_frame({#frame_header{type=?WINDOW_UPDATE, stream_id=StreamId},
#window_update{
window_size_increment=WSI
}
},
{State=#stream_state{
stream_id=StreamId,
send_window_size=SWS,
queued_frames=QF
},
ConnectionState}) ->
NewSendWindow = WSI + SWS,
NewState = State#stream_state{
send_window_size=NewSendWindow,
queued_frames=[]
},
lists:foldl(
fun(Frame, StreamAndConn) -> send_frame(Frame, StreamAndConn) end,
{NewState, ConnectionState},
QF);
route_frame({H=#frame_header{stream_id=0}, #window_update{window_size_increment=WSI}},
S = #connection_state{
socket=_Socket,
send_window_size=SWS
})
when H#frame_header.type == ?WINDOW_UPDATE ->
{next_state, connected, S#connection_state{send_window_size=SWS+WSI}};
A client can request that a server prioritizes a certain stream over another.
It is built in a tree structure so you can create requests for stream dependencies.
Leaves in a tree can have different weights
Sent to change a stream's priority
Each response on a new stream
Server initiated streams have even identifiers
What even are stream identifiers?
Imagine inspecting HTML on the way out, and sending pushes for all the CSS, JavaScript, and Images you need.
Is this exciting for APIs? Yes!
Headers for a Server Push message
Can also follow a PUSH_PROMISE
+-------------------------+
The client connection preface starts with a sequence of 24 octets, which in hex notation is: 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a That is, the connection preface starts with the string "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" This sequence MUST be followed by a SETTINGS frame (Section 6.5), which MAY be empty.
The server connection preface consists of a potentially empty SETTINGS frame (Section 6.5) that MUST be the first frame the server sends in the HTTP/2 connection. The SETTINGS frames received from a peer as part of the connection preface MUST be acknowledged (see Section 6.5.3) after sending the connection preface.
Once all values have been processed, the recipient MUST immediately emit a SETTINGS frame with the ACK flag set. Upon receiving a SETTINGS frame with the ACK flag set, the sender of the altered parameters can rely on the setting having been applied. If the sender of a SETTINGS frame does not receive an acknowledgement within a reasonable amount of time, it MAY issue a connection error (Section 5.4.1) of type SETTINGS_TIMEOUT.
A connection start must
Once the HTTP/2 connection is established, endpoints can begin exchanging frames.
-define(PREAMBLE, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n").
handle_info({_, _Socket, <<?PREAMBLE,Bin/bits>>}, _, S) ->
gen_fsm:send_event(self(), {start_frame,Bin}),
{next_state, settings_handshake, S};
settings_handshake({start_frame, <<>>},
S = #connection_state{
socket=Socket,
recv_settings=ServerSettings
}) ->
http2_frame_settings:send(Socket, ServerSettings),
NewState = S#http2_connection_state{
frame_backlog=queue:new()
},
settings_handshake_loop({false,false}, [], NewState).
settings_handshake_loop({true, true}, [], State) ->
gen_fsm:send_event(self(), backlog),
{next_state, connected, State};
settings_handshake_loop(Done, [], State=#connection_state{
socket=Socket,
frame_backlog=FB
}) ->
Frame = {FH, _} = http2_frame:read(Socket),
case FH#frame_header.type of
?SETTINGS ->
settings_handshake_loop(Done, [Frame], State);
_ ->
%% loop right back into this state after putting one on the backlog
settings_handshake_loop(Done, [], State#connection_state{
frame_backlog=queue:in(Frame, FB)
})
end;
settings_handshake_loop({_ReceivedAck, ReceivedClientSettings},
[{FH, _FPayload}|SettingsFramesTail],
State)
when ?IS_FLAG(FH#frame_header.flags, ?FLAG_ACK) ->
settings_handshake_loop({true, ReceivedClientSettings},
SettingsFramesTail,
State);
settings_handshake_loop({ReceivedAck, _ReceivedClientSettings},
[{_FH, FPayload}|SettingsFramesTail],
S=#connection_state{}) ->
ClientSettings = http2_frame_settings:overlay(S#connection_state.send_settings, FPayload),
http2_frame_settings:ack(S#connection_state.socket),
settings_handshake_loop({ReceivedAck, true},
SettingsFramesTail,
S#connection_state{
send_settings=ClientSettings
}).
No content handler, just a hard coded response
This Slide Deck!
Building a client that breaks the rules
RFC 7540 Section 4.3
Each header block is processed as a discrete unit. Header blocks MUST be transmitted as a contiguous sequence of frames, with no interleaved frames of any other type or from any other stream. The last frame in a sequence of HEADERS or CONTINUATION frames has the END_HEADERS flag set. The last frame in a sequence of PUSH_PROMISE or CONTINUATION frames has the END_HEADERS flag set. This allows a header block to be logically equivalent to a single frame.
RFC 7540 Section 6.2
END_HEADERS (0x4): When set, bit 2 indicates that this frame contains an entire header block (Section 4.3) and is not followed by any CONTINUATION frames. A HEADERS frame without the END_HEADERS flag set MUST be followed by a CONTINUATION frame for the same stream. A receiver MUST treat the receipt of any other type of frame or a frame on a different stream as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
Once a HEADERS frame is received on Stream X, only accept CONTINUATION frames on Stream X until one comes over with the END_HEADERS flag set
If any other frame type, or frames from any other Stream come over, send a GOAWAY frame with a PROTOCOL_ERROR code
Reading RFCs so you don't have to, since 2015
Frames = [
{#frame_header{length=8,type=?HEADERS,stream_id=3},
#headers{ block_fragment=H1 }},
{#frame_header{length=8,type=?CONTINUATION,stream_id=3},
#continuation{block_fragment=H2}},
%% not allowed!
{#frame_header{length=8,type=?HEADERS,stream_id=3},
#headers{block_fragment=H1}},
{#frame_header{
length=8,type=?CONTINUATION,
flags=?FLAG_END_HEADERS,
stream_id=3
},
#continuation{block_fragment=H3}}
],
http2c:send_unaltered_frames(Client, Frames),
%% No response on stream 3
Resp = http2c:get_frames(Client, 3),
?assertEqual(0, length(Resp)),
%% One GOAWAY frame on stream 0
Resp2 = http2c:get_frames(Client, 0),
1 = length(Resp2),
%% Protocol Error
[{_GoAwayH, GoAway}] = Resp2,
?PROTOCOL_ERROR = GoAway#goaway.error_code,
continuation(start_frame,
S = #connection_state{
socket=Socket,
continuation_stream_id = StreamId
}) ->
Frame = {FH,_} = http2_frame:read(Socket),
Response = case {FH#frame_header.stream_id, FH#frame_header.type} of
{StreamId, ?CONTINUATION} ->
route_frame(Frame, S);
_ ->
go_away(?PROTOCOL_ERROR, S)
end,
gen_fsm:send_event(self(), start_frame),
Response;
continuation(_, State) ->
go_away(?PROTOCOL_ERROR, State).
Resp = http2c:get_frames(Client, 3),
?assertEqual(2, length(Resp)).
Stream0 = http2c:get_frames(Client, 0),
?assertEqual(0, length(Stream0)),
p.s. there are a bunch of references later, keep reading!