Take a Chance
on HTTP

Chatterbox: HTTP/2 for Erlang

Joe DeVivo / @joedevivo

Chatterbox

Here's the code!

Here's this slide deck! joedevivo.com/euc2015

A Brief History of HTTP

On HTTP/1.1's TCP Usage

It uses less connections because 'Keepalive'

That's good!

It sequentially sends requests and receives responses in order via Pipelining

That's bad!

Well, sometimes. Be responsible

Head of line Blocking

  • Pipelining lets you send another request before the first one responds
  • It will not respond until the first response is fully served
  • Most browsers disable pipelining by default

Performance Hacks

  • Spriting
  • Inlining
  • Concatenation
  • Sharding

Transfer Sizes

Dec/2010 - Apr/2015

HTTP/2 Abstract

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.

High Level HTTP/2

  • more efficient use of network resources
  • reduced perception of latency
  • header field compression
  • allowing multiple concurrent exchanges on the same connection
  • unsolicited push of representations from servers
  • HTTP’s existing semantics remain unchanged.

SPDY

Google tried to fix this and pretty much proved it could work

HTTP/2

2015 - RFC 7540

  • Keep your semantics
  • A path to upgrading
  • Be more efficient with connections / networking
  • and Server Push!

Keep your semantics

  • methods: GET, POST, and Friends
  • Headers
  • Requests
  • Responses

Technically that means

Erlang

Upgrading HTTP/1* Requests

https://

Uses TLS's "Next Protocol Negotiation"

Or TLS's "Application Layer Protocol Negotiation"

http://

Uses "Upgrade: " header.

HTTP/2 servers respond with status code

101 Switching

Networking Optimizations

Header Compression

HTTP is stateless

Stateless protocols are repetitive

Stateless protocols are repetitive

HPACK - RFC 7541

A whole RFC just for header compression!

Compression Context is Stateful

What is a compression context?

Lookup table for common and recently used headers

The Static Table

                    +-------+--------------------+---------------+
                    | 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   |               |
                    +-------+--------------------+---------------+
headers.erl:62-123
                      [{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}]

psuedo-headers

  • :method
  • :path
  • :scheme
  • :status

Initial Context

is

the Static Table

The Dynamic Table

Add your own!

Indexes 62+

Bounded by size in HTTP/2 connection settings as a security precaution

headers.erl:15-27

-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{}.
hpack.erl

-spec encode([{binary(), binary()}], encode_context()) -> {binary(), encode_context()}.
-spec decode(binary(), decode_context()) -> {headers(), decode_context()}.

Encoding any header that already exists in the context doesn't change the context


StaticTable = hpack:new_encode_context(),
{HeaderBin, StaticTable} = encode([{<<":status">>, <<"200">>}], StaticTable).

StaticTable = hpack:new_decode_context(),
{[{<<":status">>, <<"200">>}], StaticTable} = decode(HeaderBin, StaticTable).

Encoding any header that doesn't already exists in the context changes the context


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!

There Are Four Contexts!

Given two peers: X & Y, connected over C1

  • Context A1: encoding outbound requests on X to Y over C1
  • Context A2: decoding inbound requests on Y from X over C1
  • Context B1: encoding outbound responses on Y to X over C1
  • Context B2: decoding inbound responses on X from Y over C1

MATH!

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'

The Basic Case

                    +---------------+           +---------------+
                    |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  | |
| +----------+   +-----------+   +--+-----------+--+   +-----------+   +----------+ |
|                                   |           |                                   |
|                                   |           |                                   |
+-----------------------------------+           +-----------------------------------+

A More interesting case

                    +---------------+           +---------------+
                    |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| |
| +----------+   +-----------+   +--+-----------+--+   +-----------+   +----------+ |
+-----------------------------------+           +-----------------------------------+

How "H" Packs

Data Types

  • Numbers
  • Strings

Indexed Header Field

                            0   1   2   3   4   5   6   7
                          +---+---+---+---+---+---+---+---+
                          | 1 |        Index (7+)         |
                          +---+---------------------------+
            

Literal Header Field w/ Index

                            0   1   2   3   4   5   6   7
                          +---+---+---+---+---+---+---+---+
                          | 0 | 1 |      Index (6+)       |
                          +---+---+-----------------------+
                          | H |     Value Length (7+)     |
                          +---+---------------------------+
                          | Value String (Length octets)  |
                          +-------------------------------+
            

Literal Header Field w/ Index

                            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)  |
                          +-------------------------------+
            

Types of Literal Fields

  • with Indexing - added to the dynamic table
  • without Indexing - not added to the DT
  • never Indexed - never added to any DT

Erlang

hpack:decode

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);

Huffman Encoding

Examples

There are tons of cool examples in HPACK: Appendix C

They're so good that I turned them into EUnit tests

HPACK Tables Example

http2_frame_size_SUITE.erl#L39-L70

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),
  

Request 1


                Headers1 = [
                 {<<":path">>, <<"/">>},
                 {<<"user-agent">>, <<"my cool browser">>},
                 {<<"x-custom-header">>, <<"some custom value">>}
                ],
  

Wiresharked R1

        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

R1 Context updates


DynamicTable = [
                {62,<<"x-custom-header">>,<<"some custom value">>},
                {63,<<"user-agent">>,     <<"my cool browser">>}
              ]
  • :path changes nothing
  • "user-agent"/"my cool browser" is now Index 62
  • "x-custom-header"/"some custom value" is now Index 62
  • "user-agent"/"my cool browser" is +1'd to 63

Request 2


               Headers2 = [
                {<<":path">>, <<"/some_file.html">>},
                {<<"user-agent">>, <<"my cool browser">>},
                {<<"x-custom-header">>, <<"some custom value">>}
               ],
  

Wiresharked R2

        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

R2: Context updates


              [
                {62,<<":path">>,          <<"/some_file.html">>},
                {63,<<"x-custom-header">>,<<"some custom value">>},
                {64,<<"user-agent">>,     <<"my cool browser">>}
              ]
  • ":path"/"/some_file.html" is the new Index 62
  • "x-custom-header"/"some custom value" is +1'd 63
  • "user-agent"/"my cool browser" is +1'd to 64

Request 3


                    Headers3 = [
                     {<<":path">>, <<"/some_file.html">>},
                     {<<"user-agent">>, <<"my cool browser">>},
                     {<<"x-custom-header">>, <<"new value">>}
                    ],
  

Wiresharked R3

       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

R3: Context updates


                [
                  {62,<<"x-custom-header">>,<<"new value">>},
                  {63,<<":path">>,          <<"/some_file.html">>},
                  {64,<<"x-custom-header">>,<<"some custom value">>},
                  {65,<<"user-agent">>,     <<"my cool browser">>}
                ]
  • "x-custom-header"/"new value" is the new 62
  • ":path"/"/some_file.html" is +1'd to 63
  • "x-custom-header"/"some custom value" is +1'd 64
  • "user-agent"/"my cool browser" is +1'd to 65

Multiplexing

Pipelining done right

Streams

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

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.

Frames

A frame's stream identifier determines
which multiplexed stream it belongs to

One Stream Id

per frame

A series of frames for each stream id is reconstructed on the other side, in the order they arrive

Order?

Order Matters

+---------------+           +---------------+
|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  | |
---------+   +--+-----------+--+              +-----------+   +----------+ |
----------------+           +----------------------------------------------+

Jargon Alert!

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

Handling

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   |
            `----------------------->|        |<----------------------'
                                     +--------+
            

Frames

        +-----------------------------------------------+
        |                 Length (24)                   |
        +---------------+---------------+---------------+
        |   Type (8)    |   Flags (8)   |
        +-+-------------+---------------+-------------------------------+
        |R|                 Stream Identifier (31)                      |
        +=+=============================================================+
        |                   Frame Payload (0...)                      ...
        +---------------------------------------------------------------+
            

Type

One of 10 frame types. Each has rules for payload sizes, payload content and which flags can be set.

Flags

A few control bits that have different uses for different frame types

R

A shoutout to pirates

Stream Identifier

Which multiplexed stream this frame is for

        +-----------------------------------------------+
        |                 Length (24)                   |
        +---------------+---------------+---------------+
        |   Type (8)    |   Flags (8)   |
        +-+-------------+---------------+-------------------------------+
        |R|                 Stream Identifier (31)                      |
        +=+=============================================================+
        |                   Frame Payload (0...)                      ...
        +---------------------------------------------------------------+

Erlang

http2_frame:read_binary_frame_header/1

-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}.

Connection Level Frame Types

SETTINGS

Negotiates overall connection settings

PING

measuring round trip, also keeps the connection open

GOAWAY

We're done here. Could be an error, could just be natual causes

Stream Level Frame Types

HEADERS

Request and Response Headers start with these

CONTINUATION

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

DATA

Request and Response Bodies are made of these

RST_STREAM

Sent to give up on a stream

A Sample Message

                              +-------------------------+
                              |HEADERS                  |
                              +-------------------------+
                              |CONTINUATION             |
                              +-------------------------+
                              |CONTINUATION  END_HEADERS|
                              +-------------------------+
                              |DATA                     |
                              +-------------------------+
                              |DATA                     |
                              +-------------------------+
                              |DATA          END_STREAM |
                              +-------------------------+
            

Routing Individual Frames

http2_connection

http2_connection.erl

Frame Size Errors

http2_connection:route_frame

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);

STREAMS

http2_stream

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   |
            `----------------------->|        |<----------------------'
                                     +--------+
            

Stream State

http2.hrl:204-211

-record(stream_state, {
  stream_id = undefined :: stream_id(),
  state = idle :: stream_state_name()
}).

Stream Transitions

http2_stream.erl

-spec recv_frame(frame(), {stream_state(), connection_state()}) ->
                                      {stream_state(), connection_state()}.
-spec send_frame(frame(), {stream_state(), connection_state()}) ->
                                      {stream_state(), connection_state()}.

routing a headers frame

http2_connection:route_frame

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),

http2_stream

State Machine without a Process

new/1


-spec new(stream_id()) -> stream_state().
new(StreamId) ->
    #stream_state{
       stream_id=StreamId
      }.
http2_connection:route_frame

    {Stream2, NextConnectionState} =
        http2_stream:recv_frame(
                F,
                {Stream,
                 S#connection_state{
                                     decode_context=NewDecodeContext
                                  }}),

http2_stream:recv_frame

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};
http2_stream:recv_frame

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};
http2_connection:route_frame

    {NewStreamState, NewConnectionState} =
        Handler:handle(
          NextConnectionState,
          Headers,
          Stream2),
    {next_state, connected, NewConnectionState#connection_state{
                              streams = [{StreamId, NewStreamState}|Streams]
                             }};

Flow Control

Applied to both the Connection as a whole and Individual Streams

Only Applies to Data Frames

Receiver

Each receiver advertises how many bytes it can receive

Sender

Won't send more than the receiver advertises

WINDOW_UPDATE

NEW FRAME TYPE!

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

Implementing Flow Control

Sending Response Data

chatterbox_static_content_handler

            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]};
chatterbox_static_content_handler

      %% 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).
http2_stream:send_frame

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
      }
    };
http2_stream:send_frame

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};
http2_stream:recv_frame(?WINDOW_UPDATE)

%% 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);
http2_connection:route_frame

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}};

Prioritization

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

A server is under no obligation to honor these requests

PRIORITY

NEW FRAME TYPE

Sent to change a stream's priority

Server Push

Multiple Responses per Request

Each response on a new stream

Server initiated streams have even identifiers

What even are stream identifiers?

Static Content

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!

PUSH_PROMISE

NEW FRAME TYPE

Headers for a Server Push message

CONTINUATION

Can also follow a PUSH_PROMISE

A Sample Push Message

                             +-------------------------+
            

Without a connection, what can we do?

RFC 7540 - Section 3.5

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.
            

RFC 7540 - Section 3.5

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.
            

RFC 7540 - Section 6.5.3

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.
            

tl;dr

A connection start must

  • Send the TEXT: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
  • Must send a SETTINGS frame
  • Must have that SETTINGS frame ACK'd
  • Must receive a SETTINGS frame from the client
  • Must ACK that client's SETTINGS frame

SETTINGS

  • SETTINGS_HEADER_TABLE_SIZE
  • SETTINGS_ENABLE_PUSH
  • SETTINGS_MAX_CONCURRENT_STREAMS
  • SETTINGS_INITIAL_WINDOW_SIZE
  • SETTINGS_MAX_FRAME_SIZE
  • SETTINGS_MAX_HEADER_LIST_SIZE

RFC 7540 - Section 4

Once the HTTP/2 connection is established, endpoints can begin
exchanging frames.
            
http2_connection:handle_info

-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 State

What it does it

  1. Send our server SETTINGS frame
  2. Wait for client SETTINGS frame
  3. Wait client's SETTINGS ACK
  4. Send ACK of client settings
  5. Backlog any frames before 3 and 4 are done
  6. Transition to connected state
http2_connection:settings_handshake/2

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/3

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/3

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/3

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
                             }).

Development

Minimum Viable Response: NGHTTP2

No content handler, just a hard coded response

Firefox 37

This Slide Deck!

When Things Go Wrong

Building a client that breaks the rules

http2c APIs

  • High Level - Request/Response Message Level (Keep your semantics!)
  • Mid Level - HTTP/2 Frames
  • Low Level - Binary

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.

TL;DR

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

Joe DeVivo

Reading RFCs so you don't have to, since 2015

header_continuation_SUITE

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),
header_continuation_SUITE

%% 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,

http2_connection:

continuation state

  • Any time a HEADERS frame is received, enter CONTINUATION state
  • In that state, only accept CONTINUATION frames from that stream
  • when a CONTINUATION with the END HEADERS flag is set, transition back to 'connected'
  • If any other frame type comes in (or CONTINUATION with other stream id), send GOAWAY with CONNECTION ERROR

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).

Take out the bad frame

Successful Test!

Resp = http2c:get_frames(Client, 3),
?assertEqual(2, length(Resp)).

Stream0 = http2c:get_frames(Client, 0),
?assertEqual(0, length(Stream0)),

THANKS!

p.s. there are a bunch of references later, keep reading!

Here's the code again!

References

HTTP History References

HTTP/1.x RFCs

HTTP/2 RFCs