@@ -24,6 +24,69 @@ defmodule Mint.HTTP2 do
2424 does not mean that the response to request A will come before the response to request B.
2525 This is why we identify each request with a unique reference returned by `request/5`.
2626 See `request/5` for more information.
27+
28+ ## Server push
29+
30+ HTTP/2 supports [server push](https://en.wikipedia.org/wiki/HTTP/2_Server_Push), which
31+ is a way for a server to send a response to a client without the client needing to make
32+ the corresponding request. The server sends a `:push_promise` response to a normal request:
33+ this creates a new request reference. Then, the server sends normal responses for the newly
34+ created request reference.
35+
36+ Let's see an example. We will ask the server for `"/index.html"` and the server will
37+ send us a push promise for `"/style.css"`.
38+
39+ {:ok, conn} = Mint.HTTP2.connect(:https, "example.com", 443)
40+ {:ok, conn, request_ref} = Mint.HTTP2.request(conn, "GET", "/index.html", _headers = [])
41+
42+ next_message =
43+ receive do
44+ msg -> msg
45+ end
46+
47+ {:ok, conn, responses} = Mint.HTTP2.stream(conn, next_message)
48+
49+ [
50+ {:push_promise, ^request_ref, promised_request_ref, promised_headers},
51+ {:status, ^request_ref, 200},
52+ {:headers, ^request_ref, []},
53+ {:data, ^request_ref, "<html>..."},
54+ {:done, ^request_ref}
55+ ] = responses
56+
57+ promised_headers
58+ #=> [{":method", "GET"}, {":path", "/style.css"}]
59+
60+ As you can see in the example above, when the server sends a push promise then a
61+ `:push_promise` response is returned as a response to a request. The `:push_promise`
62+ response contains a `promised_request_ref` and some `promised_headers`. The
63+ `promised_request_ref` is the new request ref that pushed responses will be tagged with.
64+ `promised_headers` are headers that tell the client *what request* the promised response
65+ will respond to. The idea is that the server tells the client a request the client will
66+ want to make and then preemptively sends a response for that request. Promised headers
67+ will always include `:method`, `:path`, and `:authority`.
68+
69+ next_message =
70+ receive do
71+ msg -> msg
72+ end
73+
74+ {:ok, conn, responses} = Mint.HTTP2.stream(conn, next_message)
75+
76+ [
77+ {:status, ^promised_request_ref, 200},
78+ {:headers, ^promised_request_ref, []},
79+ {:data, ^promised_request_ref, "body { ... }"},
80+ {:done, ^promised_request_ref}
81+ ]
82+
83+ The response to a promised request is like a response to any normal request.
84+
85+ ### Disabling server pushes
86+
87+ HTTP/2 exposes a boolean setting for enabling or disabling server pushes with `:enable_push`.
88+ You can pass this option when connecting or in `put_settings/2`. By default server push
89+ is enabled.
2790 """
2891
2992 use Bitwise , skip_operators: true
@@ -39,6 +102,7 @@ defmodule Mint.HTTP2 do
39102 }
40103
41104 require Logger
105+ require Integer
42106
43107 @ behaviour Mint.Core.Conn
44108
@@ -65,7 +129,7 @@ defmodule Mint.HTTP2 do
65129 :port ,
66130 :scheme ,
67131
68- # Mint.HTTP2ection state (open, closed, and so on).
132+ # Connection state (open, closed, and so on).
69133 :state ,
70134
71135 # Fields of the connection.
@@ -95,6 +159,7 @@ defmodule Mint.HTTP2 do
95159 # SETTINGS-related things for client.
96160 client_max_frame_size: @ default_max_frame_size ,
97161 client_max_concurrent_streams: 100 ,
162+ client_enable_push: true ,
98163
99164 # Headers being processed (when headers are split into multiple frames with CONTINUATIONS, all
100165 # the continuation frames must come one right after the other).
@@ -131,7 +196,7 @@ defmodule Mint.HTTP2 do
131196 server_max_concurrent_streams: non_neg_integer ( ) ,
132197 initial_window_size: pos_integer ( ) ,
133198 max_frame_size: pos_integer ( ) ,
134- headers_being_processed: { stream_id :: non_neg_integer ( ) , iodata ( ) , boolean ( ) } | nil
199+ headers_being_processed: { stream_id :: non_neg_integer ( ) , iodata ( ) , fun ( ) } | nil
135200 }
136201
137202 ## Public interface
@@ -707,16 +772,8 @@ defmodule Mint.HTTP2 do
707772 end
708773
709774 { :enable_push , value } ->
710- case value do
711- true ->
712- raise ArgumentError ,
713- "push promises are not supported yet, so :enable_push must be false"
714-
715- false ->
716- :ok
717-
718- _other ->
719- raise ArgumentError , ":enable_push must be a boolean, got: #{ inspect ( value ) } "
775+ unless is_boolean ( value ) do
776+ raise ArgumentError , ":enable_push must be a boolean, got: #{ inspect ( value ) } "
720777 end
721778
722779 { :max_concurrent_streams , value } ->
@@ -847,8 +904,8 @@ defmodule Mint.HTTP2 do
847904
848905 defp handle_frame ( conn , settings ( ) = frame , resps ) , do: handle_settings ( conn , frame , resps )
849906
850- # TODO: implement PUSH_PROMISE
851- defp handle_frame ( _ , push_promise ( ) , _resps ) , do: raise ( "PUSH_PROMISE handling not implemented" )
907+ defp handle_frame ( conn , push_promise ( ) = frame , resps ) ,
908+ do: handle_push_promise ( conn , frame , resps )
852909
853910 defp handle_frame ( conn , Frame . ping ( ) = frame , resps ) , do: handle_ping ( conn , frame , resps )
854911
@@ -892,32 +949,42 @@ defmodule Mint.HTTP2 do
892949 defp handle_headers ( conn , frame , responses ) do
893950 headers ( stream_id: stream_id , flags: flags , hbf: hbf ) = frame
894951 stream = fetch_stream! ( conn , stream_id )
895- assert_stream_in_state ( conn , stream , [ :open , :half_closed_local ] )
952+ assert_stream_in_state ( conn , stream , [ :open , :half_closed_local , :reserved_remote ] )
896953 end_stream? = flag_set? ( flags , :headers , :end_stream )
897954
898955 if flag_set? ( flags , :headers , :end_headers ) do
899956 decode_hbf_and_add_responses ( conn , responses , hbf , stream , end_stream? )
900957 else
901- conn = put_in ( conn . headers_being_processed , { stream_id , hbf , end_stream? } )
958+ callback = & decode_hbf_and_add_responses ( & 1 , & 2 , & 3 , & 4 , end_stream? )
959+ conn = put_in ( conn . headers_being_processed , { stream_id , hbf , callback } )
902960 { conn , responses }
903961 end
904962 end
905963
906964 defp decode_hbf_and_add_responses ( conn , responses , hbf , stream , end_stream? ) do
907965 case decode_hbf ( conn , hbf ) do
908- { :ok , status , headers , conn } ->
966+ { :ok , [ { ":status" , status } | headers ] , conn } ->
967+ status = String . to_integer ( status )
909968 responses = [ { :headers , stream . ref , headers } , { :status , stream . ref , status } | responses ]
910969
911- if end_stream? do
912- conn = put_in ( conn . streams [ stream . id ] . state , :half_closed_remote )
913- conn = update_in ( conn . open_stream_count , & ( & 1 - 1 ) )
914- { conn , [ { :done , stream . ref } | responses ] }
915- else
916- { conn , responses }
970+ cond do
971+ end_stream? ->
972+ conn = put_in ( conn . streams [ stream . id ] . state , :half_closed_remote )
973+ conn = update_in ( conn . open_stream_count , & ( & 1 - 1 ) )
974+ { conn , [ { :done , stream . ref } | responses ] }
975+
976+ # If this was a promised stream, it goes in the :half_closed_local state as
977+ # soon as it receives headers.
978+ stream . state == :reserved_remote ->
979+ conn = put_in ( conn . streams [ stream . id ] . state , :half_closed_local )
980+ { conn , responses }
981+
982+ true ->
983+ { conn , responses }
917984 end
918985
919986 # http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.6
920- { :error , :missing_status_header , conn } ->
987+ { :ok , _headers , conn } ->
921988 conn = close_stream! ( conn , stream . id , :protocol_error )
922989 reason = { :protocol_error , :missing_status_header }
923990 responses = [ { :error , stream . ref , reason } | responses ]
@@ -929,11 +996,7 @@ defmodule Mint.HTTP2 do
929996 case HPACK . decode ( hbf , conn . decode_table ) do
930997 { :ok , headers , decode_table } ->
931998 conn = put_in ( conn . decode_table , decode_table )
932-
933- case headers do
934- [ { ":status" , status } | headers ] -> { :ok , String . to_integer ( status ) , headers , conn }
935- _other -> { :error , :missing_status_header , conn }
936- end
999+ { :ok , headers , conn }
9371000
9381001 { :error , reason } ->
9391002 debug_data = "unable to decode headers: #{ inspect ( reason ) } "
@@ -1017,6 +1080,9 @@ defmodule Mint.HTTP2 do
10171080
10181081 { :max_concurrent_streams , value } , conn ->
10191082 put_in ( conn . client_max_concurrent_streams , value )
1083+
1084+ { :enable_push , value } , conn ->
1085+ put_in ( conn . client_enable_push , value )
10201086 end )
10211087 end
10221088
@@ -1042,6 +1108,71 @@ defmodule Mint.HTTP2 do
10421108 put_in ( conn . initial_window_size , new_iws )
10431109 end
10441110
1111+ # PUSH_PROMISE
1112+
1113+ defp handle_push_promise (
1114+ % Mint.HTTP2 { client_enable_push: false } = conn ,
1115+ push_promise ( ) ,
1116+ _responses
1117+ ) do
1118+ debug_data = "received PUSH_PROMISE frame when SETTINGS_ENABLE_PUSH was false"
1119+ send_connection_error! ( conn , :protocol_error , debug_data )
1120+ end
1121+
1122+ defp handle_push_promise ( conn , push_promise ( ) = frame , responses ) do
1123+ push_promise (
1124+ stream_id: stream_id ,
1125+ flags: flags ,
1126+ promised_stream_id: promised_stream_id ,
1127+ hbf: hbf
1128+ ) = frame
1129+
1130+ # TODO: improve this by checking that the promised_stream_id is not in use or has been used
1131+ # before.
1132+ unless promised_stream_id > 0 and Integer . is_even ( promised_stream_id ) do
1133+ debug_data = "invalid promised stream ID: #{ inspect ( promised_stream_id ) } "
1134+ send_connection_error! ( conn , :protocol_error , debug_data )
1135+ end
1136+
1137+ stream = fetch_stream! ( conn , stream_id )
1138+ assert_stream_in_state ( conn , stream , [ :open , :half_closed_local ] )
1139+
1140+ if flag_set? ( flags , :push_promise , :end_headers ) do
1141+ decode_push_promise_headers_and_add_response (
1142+ conn ,
1143+ responses ,
1144+ hbf ,
1145+ stream ,
1146+ promised_stream_id
1147+ )
1148+ else
1149+ callback = & decode_push_promise_headers_and_add_response ( & 1 , & 2 , & 3 , & 4 , promised_stream_id )
1150+ conn = put_in ( conn . headers_being_processed , { stream_id , hbf , callback } )
1151+ { conn , responses }
1152+ end
1153+ end
1154+
1155+ defp decode_push_promise_headers_and_add_response (
1156+ conn ,
1157+ responses ,
1158+ hbf ,
1159+ stream ,
1160+ promised_stream_id
1161+ ) do
1162+ { :ok , headers , conn } = decode_hbf ( conn , hbf )
1163+
1164+ promised_stream = % {
1165+ id: promised_stream_id ,
1166+ ref: make_ref ( ) ,
1167+ state: :reserved_remote ,
1168+ window_size: conn . initial_window_size
1169+ }
1170+
1171+ conn = put_in ( conn . streams [ promised_stream . id ] , promised_stream )
1172+ new_response = { :push_promise , stream . ref , promised_stream . ref , headers }
1173+ { conn , [ new_response | responses ] }
1174+ end
1175+
10451176 # PING
10461177
10471178 defp handle_ping ( conn , Frame . ping ( ) = frame , responses ) do
@@ -1137,13 +1268,14 @@ defmodule Mint.HTTP2 do
11371268 continuation ( stream_id: stream_id , flags: flags , hbf: hbf_chunk ) = frame
11381269 stream = fetch_stream! ( conn , stream_id )
11391270
1140- { ^ stream_id , hbf_acc , end_stream? } = conn . headers_being_processed
1271+ { ^ stream_id , hbf_acc , callback } = conn . headers_being_processed
11411272
11421273 if flag_set? ( flags , :continuation , :end_headers ) do
11431274 hbf = IO . iodata_to_binary ( [ hbf_acc , hbf_chunk ] )
1144- decode_hbf_and_add_responses ( conn , responses , hbf , stream , end_stream? )
1275+ conn = put_in ( conn . headers_being_processed , nil )
1276+ callback . ( conn , responses , hbf , stream )
11451277 else
1146- conn = put_in ( conn . headers_being_processed , { stream_id , [ hbf_acc , hbf_chunk ] , end_stream? } )
1278+ conn = put_in ( conn . headers_being_processed , { stream_id , [ hbf_acc , hbf_chunk ] , callback } )
11471279 { conn , responses }
11481280 end
11491281 end
0 commit comments