-
Notifications
You must be signed in to change notification settings - Fork 114
/
Copy pathhttp2.ex
2266 lines (1817 loc) · 77.7 KB
/
http2.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
defmodule Mint.HTTP2 do
@moduledoc """
Process-less HTTP/2 client connection.
This module provides a data structure that represents an HTTP/2 connection to
a given server. The connection is represented as an opaque struct `%Mint.HTTP2{}`.
The connection is a data structure and is not backed by a process, and all the
connection handling happens in the process that creates the struct.
This module and data structure work exactly like the ones described in the `Mint.HTTP`
module, with the exception that `Mint.HTTP2` specifically deals with HTTP/2 while
`Mint.HTTP` deals seamlessly with HTTP/1.1 and HTTP/2. For more information on
how to use the data structure and client architecture, see `Mint.HTTP`.
## HTTP/2 Streams and Requests
HTTP/2 introduces the concept of **streams**. A stream is an isolated conversation
between the client and the server. Each stream is unique and identified by a unique
**stream ID**, which means that there's no order when data comes on different streams
since they can be identified uniquely. A stream closely corresponds to a request, so
in this documentation and client we will mostly refer to streams as "requests".
We mentioned data on streams can come in arbitrary order, and streams are requests,
so the practical effect of this is that performing request A and then request B
does not mean that the response to request A will come before the response to request B.
This is why we identify each request with a unique reference returned by `request/5`.
See `request/5` for more information.
## Closed Connection
In HTTP/2, the connection can either be open, closed, or only closed for writing.
When a connection is closed for writing, the client cannot send requests or stream
body chunks, but it can still read data that the server might be sending. When the
connection gets closed on the writing side, a `:server_closed_connection` error is
returned. `{:error, request_ref, error}` is returned for requests that haven't been
processed by the server, with the reason of `error` being `:unprocessed`.
These requests are safe to retry.
## HTTP/2 Settings
HTTP/2 supports settings negotiation between servers and clients. The server advertises
its settings to the client and the client advertises its settings to the server. A peer
(server or client) has to acknowledge the settings advertised by the other peer before
those settings come into action (that's why it's called a negotiation).
A first settings negotiation happens right when the connection starts.
Servers and clients can renegotiate settings at any time during the life of the
connection.
Mint users don't need to care about settings acknowledgements directly since they're
handled transparently by `stream/2`.
To retrieve the server settings, you can use `get_server_setting/2`. Doing so is often
useful to be able to tune your requests based on the server settings.
To communicate client settings to the server, use `put_settings/2` or pass them when
starting up a connection with `connect/4`. Note that the server needs to acknowledge
the settings sent through `put_setting/2` before those settings come into effect. The
server ack is processed transparently by `stream/2`, but this means that if you change
a setting through `put_settings/2` and try to retrieve the value of that setting right
after with `get_client_setting/2`, you'll likely get the old value of that setting. Once
the server acknowledges the new settings, the updated value will be returned by
`get_client_setting/2`.
## Server Push
HTTP/2 supports [server push](https://en.wikipedia.org/wiki/HTTP/2_Server_Push), which
is a way for a server to send a response to a client without the client needing to make
the corresponding request. The server sends a `:push_promise` response to a normal request:
this creates a new request reference. Then, the server sends normal responses for the newly
created request reference.
Let's see an example. We will ask the server for `"/index.html"` and the server will
send us a push promise for `"/style.css"`.
{:ok, conn} = Mint.HTTP2.connect(:https, "example.com", 443)
{:ok, conn, request_ref} = Mint.HTTP2.request(conn, "GET", "/index.html", _headers = [], _body = "")
next_message =
receive do
msg -> msg
end
{:ok, conn, responses} = Mint.HTTP2.stream(conn, next_message)
[
{:push_promise, ^request_ref, promised_request_ref, promised_headers},
{:status, ^request_ref, 200},
{:headers, ^request_ref, []},
{:data, ^request_ref, "<html>..."},
{:done, ^request_ref}
] = responses
promised_headers
#=> [{":method", "GET"}, {":path", "/style.css"}]
As you can see in the example above, when the server sends a push promise then a
`:push_promise` response is returned as a response to a request. The `:push_promise`
response contains a `promised_request_ref` and some `promised_headers`. The
`promised_request_ref` is the new request ref that pushed responses will be tagged with.
`promised_headers` are headers that tell the client *what request* the promised response
will respond to. The idea is that the server tells the client a request the client will
want to make and then preemptively sends a response for that request. Promised headers
will always include `:method`, `:path`, and `:authority`.
next_message =
receive do
msg -> msg
end
{:ok, conn, responses} = Mint.HTTP2.stream(conn, next_message)
[
{:status, ^promised_request_ref, 200},
{:headers, ^promised_request_ref, []},
{:data, ^promised_request_ref, "body { ... }"},
{:done, ^promised_request_ref}
]
The response to a promised request is like a response to any normal request.
> #### Disabling Server Pushes {: .tip}
>
> HTTP/2 exposes a boolean setting for enabling or disabling server pushes with `:enable_push`.
> You can pass this option when connecting or in `put_settings/2`. By default server push
> is enabled.
"""
import Mint.HTTP2.Frame, except: [encode: 1, decode_next: 1, inspect: 1]
alias Mint.{HTTPError, TransportError}
alias Mint.Types
alias Mint.Core.{Headers, Util}
alias Mint.HTTP2.Frame
require Logger
require Integer
@behaviour Mint.Core.Conn
## Constants
@connection_preface "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
@transport_opts [alpn_advertised_protocols: ["h2"]]
@default_window_size 65_535
@max_window_size 2_147_483_647
@default_max_frame_size 16_384
@valid_max_frame_size_range @default_max_frame_size..16_777_215
@valid_client_settings [
:max_concurrent_streams,
:initial_window_size,
:max_frame_size,
:enable_push,
:max_header_list_size
]
@user_agent "mint/" <> Mix.Project.config()[:version]
# HTTP/2 connection struct.
defstruct [
# Transport things.
:transport,
:socket,
:mode,
# Host things.
:hostname,
:port,
:scheme,
:authority,
# Connection state (open, closed, and so on).
:state,
# Fields of the connection.
buffer: "",
window_size: @default_window_size,
encode_table: HPAX.new(4096),
decode_table: HPAX.new(4096),
# Queue for sent PING frames.
ping_queue: :queue.new(),
# Queue for sent SETTINGS frames.
client_settings_queue: :queue.new(),
# Stream-set-related things.
next_stream_id: 3,
streams: %{},
open_client_stream_count: 0,
open_server_stream_count: 0,
ref_to_stream_id: %{},
# Settings that the server communicates to the client.
server_settings: %{
enable_push: true,
max_concurrent_streams: 100,
initial_window_size: @default_window_size,
max_frame_size: @default_max_frame_size,
max_header_list_size: :infinity,
# Only supported by the server: https://www.rfc-editor.org/rfc/rfc8441.html#section-3
enable_connect_protocol: false
},
# Settings that the client communicates to the server.
client_settings: %{
max_concurrent_streams: 100,
initial_window_size: @default_window_size,
max_header_list_size: :infinity,
max_frame_size: @default_max_frame_size,
enable_push: true
},
# Headers being processed (when headers are split into multiple frames with CONTINUATIONS, all
# the continuation frames must come one right after the other).
headers_being_processed: nil,
# Stores the headers returned by the proxy in the `CONNECT` method
proxy_headers: [],
# Private store.
private: %{},
# Logging
log: false
]
defmacrop log(conn, level, message) do
quote do
conn = unquote(conn)
if conn.log do
Logger.log(unquote(level), unquote(message))
else
:ok
end
end
end
## Types
@typedoc """
HTTP/2 setting with its value.
This type represents both server settings as well as client settings. To retrieve
server settings use `get_server_setting/2` and to retrieve client settings use
`get_client_setting/2`. To send client settings to the server, see `put_settings/2`.
The supported settings are the following:
* `:header_table_size` - corresponds to `SETTINGS_HEADER_TABLE_SIZE`.
* `:enable_push` - corresponds to `SETTINGS_ENABLE_PUSH`. Sets whether
push promises are supported. If you don't want to support push promises,
use `put_settings/2` to tell the server that your client doesn't want push promises.
* `:max_concurrent_streams` - corresponds to `SETTINGS_MAX_CONCURRENT_STREAMS`.
Tells what is the maximum number of streams that the peer sending this (client or server)
supports. As mentioned in the module documentation, HTTP/2 streams are equivalent to
requests, so knowing the maximum number of streams that the server supports can be useful
to know how many concurrent requests can be open at any time. Use `get_server_setting/2`
to find out how many concurrent streams the server supports.
* `:initial_window_size` - corresponds to `SETTINGS_INITIAL_WINDOW_SIZE`.
Tells what is the value of the initial HTTP/2 window size for the peer
that sends this setting.
* `:max_frame_size` - corresponds to `SETTINGS_MAX_FRAME_SIZE`. Tells what is the
maximum size of an HTTP/2 frame for the peer that sends this setting.
* `:max_header_list_size` - corresponds to `SETTINGS_MAX_HEADER_LIST_SIZE`.
* `:enable_connect_protocol` - corresponds to `SETTINGS_ENABLE_CONNECT_PROTOCOL`.
Sets whether the client may invoke the extended connect protocol which is used to
bootstrap WebSocket connections.
"""
@type setting() ::
{:enable_push, boolean()}
| {:header_table_size, non_neg_integer()}
| {:max_concurrent_streams, pos_integer()}
| {:initial_window_size, 1..2_147_483_647}
| {:max_frame_size, 16_384..16_777_215}
| {:max_header_list_size, :infinity | pos_integer()}
| {:enable_connect_protocol, boolean()}
@typedoc """
HTTP/2 settings.
See `t:setting/0`.
"""
@type settings() :: [setting()]
@typedoc """
An HTTP/2-specific error reason.
The values can be:
* `:closed` - when you try to make a request or stream a body chunk but the connection
is closed.
* `:closed_for_writing` - when you try to make a request or stream a body chunk but
the connection is closed for writing. This means you cannot issue any more requests.
See the "Closed connection" section in the module documentation for more information.
* `:too_many_concurrent_requests` - when the maximum number of concurrent requests
allowed by the server is reached. To find out what this limit is, use `get_setting/2`
with the `:max_concurrent_streams` setting name.
* `{:max_header_list_size_exceeded, size, max_size}` - when the maximum size of
the header list is reached. `size` is the actual value of the header list size,
`max_size` is the maximum value allowed. See `get_setting/2` to retrieve the
value of the max size.
* `{:exceeds_window_size, what, window_size}` - when the data you're trying to send
exceeds the window size of the connection (if `what` is `:connection`) or of a request
(if `what` is `:request`). `window_size` is the allowed window size. See
`get_window_size/2`.
* `{:stream_not_found, stream_id}` - when the given request is not found.
* `:unknown_request_to_stream` - when you're trying to stream data on an unknown
request.
* `:request_is_not_streaming` - when you try to send data (with `stream_request_body/3`)
on a request that is not open for streaming.
* `:unprocessed` - when a request was closed because it was not processed by the server.
When this error is returned, it means that the server hasn't processed the request at all,
so it's safe to retry the given request on a different or new connection.
* `{:server_closed_request, error_code}` - when the server closes the request.
`error_code` is the reason why the request was closed.
* `{:server_closed_connection, reason, debug_data}` - when the server closes the connection
gracefully or because of an error. In HTTP/2, this corresponds to a `GOAWAY` frame.
`error` is the reason why the connection was closed. `debug_data` is additional debug data.
* `{:frame_size_error, frame}` - when there's an error with the size of a frame.
`frame` is the frame type, such as `:settings` or `:window_update`.
* `{:protocol_error, debug_data}` - when there's a protocol error.
`debug_data` is a string that explains the nature of the error.
* `{:compression_error, debug_data}` - when there's a header compression error.
`debug_data` is a string that explains the nature of the error.
* `{:flow_control_error, debug_data}` - when there's a flow control error.
`debug_data` is a string that explains the nature of the error.
"""
@type error_reason() :: term()
@typedoc """
A Mint HTTP/2 connection struct.
The struct's fields are private.
"""
@opaque t() :: %__MODULE__{}
## Public interface
@doc """
Same as `Mint.HTTP.connect/4`, but forces a HTTP/2 connection.
"""
@spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) ::
{:ok, t()} | {:error, Types.error()}
def connect(scheme, address, port, opts \\ []) do
hostname = Mint.Core.Util.hostname(opts, address)
transport_opts =
opts
|> Keyword.get(:transport_opts, [])
|> Keyword.merge(@transport_opts)
|> Keyword.put(:hostname, hostname)
case negotiate(address, port, scheme, transport_opts) do
{:ok, socket} ->
initiate(scheme, socket, hostname, port, opts)
{:error, reason} ->
{:error, reason}
end
end
@doc false
@spec upgrade(
Types.scheme(),
Mint.Types.socket(),
Types.scheme(),
String.t(),
:inet.port_number(),
keyword()
) :: {:ok, t()} | {:error, Types.error()}
def upgrade(old_scheme, socket, new_scheme, hostname, port, opts) do
transport = Util.scheme_to_transport(new_scheme)
transport_opts =
opts
|> Keyword.get(:transport_opts, [])
|> Keyword.merge(@transport_opts)
with {:ok, socket} <- transport.upgrade(socket, old_scheme, hostname, port, transport_opts) do
initiate(new_scheme, socket, hostname, port, opts)
end
end
@doc """
See `Mint.HTTP.close/1`.
"""
@impl true
@spec close(t()) :: {:ok, t()}
def close(conn)
def close(%__MODULE__{state: :open} = conn) do
send_connection_error!(conn, :no_error, "connection peacefully closed by client")
catch
{:mint, conn, %HTTPError{reason: {:no_error, _}}} ->
{:ok, conn}
end
def close(%__MODULE__{state: {:goaway, _error_code, _debug_data}} = conn) do
_ = conn.transport.close(conn.socket)
{:ok, put_in(conn.state, :closed)}
end
def close(%__MODULE__{state: :handshaking} = conn) do
_ = conn.transport.close(conn.socket)
{:ok, put_in(conn.state, :closed)}
end
def close(%__MODULE__{state: :closed} = conn) do
{:ok, conn}
end
@doc """
See `Mint.HTTP.open?/1`.
"""
@impl true
@spec open?(t(), :read | :write) :: boolean()
def open?(%__MODULE__{state: state} = _conn, type \\ :write)
when type in [:read, :write, :read_write] do
case state do
:handshaking -> true
:open -> true
{:goaway, _error_code, _debug_data} -> type == :read
:closed -> false
end
end
@doc """
See `Mint.HTTP.request/5`.
In HTTP/2, opening a request means opening a new HTTP/2 stream (see the
module documentation). This means that a request could fail because the
maximum number of concurrent streams allowed by the server has been reached.
In that case, the error reason `:too_many_concurrent_requests` is returned.
If you want to avoid incurring in this error, you can retrieve the value of
the maximum number of concurrent streams supported by the server through
`get_server_setting/2` (passing in the `:max_concurrent_streams` setting name).
## Header list size
In HTTP/2, the server can optionally specify a maximum header list size that
the client needs to respect when sending headers. The header list size is calculated
by summing the length (in bytes) of each header name plus value, plus 32 bytes for
each header. Note that pseudo-headers (like `:path` or `:method`) count towards
this size. If the size is exceeded, an error is returned. To check what the size
is, use `get_server_setting/2`.
## Request body size
If the request body size will exceed the window size of the HTTP/2 stream created by the
request or the window size of the connection Mint will return a `:exceeds_window_size`
error.
To ensure you do not exceed the window size it is recommended to stream the request
body by initially passing `:stream` as the body and sending the body in chunks using
`stream_request_body/3` and using `get_window_size/2` to get the window size of the
request and connection.
"""
@impl true
@spec request(
t(),
method :: String.t(),
path :: String.t(),
Types.headers(),
body :: iodata() | nil | :stream
) ::
{:ok, t(), Types.request_ref()}
| {:error, t(), Types.error()}
def request(conn, method, path, headers, body)
def request(%__MODULE__{state: :closed} = conn, _method, _path, _headers, _body) do
{:error, conn, wrap_error(:closed)}
end
def request(
%__MODULE__{state: {:goaway, _error_code, _debug_data}} = conn,
_method,
_path,
_headers,
_body
) do
{:error, conn, wrap_error(:closed_for_writing)}
end
def request(%__MODULE__{} = conn, method, path, headers, body)
when is_binary(method) and is_binary(path) and is_list(headers) do
headers =
headers
|> Headers.lower_raws()
|> add_pseudo_headers(conn, method, path)
|> add_default_headers(body)
|> sort_pseudo_headers_to_front()
{conn, stream_id, ref} = open_stream(conn)
{conn, payload} = encode_request_payload(conn, stream_id, headers, body)
conn = send!(conn, payload)
{:ok, conn, ref}
catch
:throw, {:mint, _conn, reason} ->
# The stream is invalid and "_conn" may be tracking it, so we return the original connection instead.
{:error, conn, reason}
end
@doc """
See `Mint.HTTP.stream_request_body/3`.
"""
@impl true
@spec stream_request_body(
t(),
Types.request_ref(),
iodata() | :eof | {:eof, trailer_headers :: Types.headers()}
) :: {:ok, t()} | {:error, t(), Types.error()}
def stream_request_body(conn, request_ref, chunk)
def stream_request_body(%__MODULE__{state: :closed} = conn, _request_ref, _chunk) do
{:error, conn, wrap_error(:closed)}
end
def stream_request_body(
%__MODULE__{state: {:goaway, _error_code, _debug_data}} = conn,
_request_ref,
_chunk
) do
{:error, conn, wrap_error(:closed_for_writing)}
end
def stream_request_body(%__MODULE__{} = conn, request_ref, chunk)
when is_reference(request_ref) do
case Map.fetch(conn.ref_to_stream_id, request_ref) do
{:ok, stream_id} ->
{conn, payload} = encode_stream_body_request_payload(conn, stream_id, chunk)
conn = send!(conn, payload)
{:ok, conn}
:error ->
{:error, conn, wrap_error(:unknown_request_to_stream)}
end
catch
:throw, {:mint, _conn, reason} ->
# The stream is invalid and "_conn" may be tracking it, so we return the original connection instead.
{:error, conn, reason}
end
@doc """
Pings the server.
This function is specific to HTTP/2 connections. It sends a **ping** request to
the server `conn` is connected to. A `{:ok, conn, request_ref}` tuple is returned,
where `conn` is the updated connection and `request_ref` is a unique reference that
identifies this ping request. The response to a ping request is returned by `stream/2`
as a `{:pong, request_ref}` tuple. If there's an error, this function returns
`{:error, conn, reason}` where `conn` is the updated connection and `reason` is the
error reason.
`payload` must be an 8-byte binary with arbitrary content. When the server responds to
a ping request, it will use that same payload. By default, the payload is an 8-byte
binary with all bits set to `0`.
Pinging can be used to measure the latency with the server and to ensure the connection
is alive and well.
## Examples
{:ok, conn, ref} = Mint.HTTP2.ping(conn)
"""
@spec ping(t(), <<_::8>>) :: {:ok, t(), Types.request_ref()} | {:error, t(), Types.error()}
def ping(%__MODULE__{} = conn, payload \\ :binary.copy(<<0>>, 8))
when byte_size(payload) == 8 do
{conn, ref} = send_ping(conn, payload)
{:ok, conn, ref}
catch
:throw, {:mint, conn, error} -> {:error, conn, error}
end
@doc """
Communicates the given **client settings** to the server.
This function is HTTP/2-specific.
This function takes a connection and a keyword list of HTTP/2 settings and sends
the values of those settings to the server. The settings won't be effective until
the server acknowledges them, which will be handled transparently by `stream/2`.
This function returns `{:ok, conn}` when sending the settings to the server is
successful, with `conn` being the updated connection. If there's an error, this
function returns `{:error, conn, reason}` with `conn` being the updated connection
and `reason` being the reason of the error.
## Supported Settings
See `t:setting/0` for the supported settings. You can see the meaning
of these settings [in the corresponding section in the HTTP/2
RFC](https://httpwg.org/specs/rfc7540.html#SettingValues).
See the "HTTP/2 settings" section in the module documentation for more information.
## Examples
{:ok, conn} = Mint.HTTP2.put_settings(conn, max_frame_size: 100)
"""
@spec put_settings(t(), settings()) :: {:ok, t()} | {:error, t(), Types.error()}
def put_settings(%__MODULE__{} = conn, settings) when is_list(settings) do
conn = send_settings(conn, settings)
{:ok, conn}
catch
:throw, {:mint, conn, error} -> {:error, conn, error}
end
@doc """
Gets the value of the given HTTP/2 server settings.
This function returns the value of the given HTTP/2 setting that the server
advertised to the client. This function is HTTP/2 specific.
For more information on HTTP/2 settings, see [the related section in
the RFC](https://httpwg.org/specs/rfc7540.html#SettingValues).
See the "HTTP/2 settings" section in the module documentation for more information.
## Supported settings
The possible settings that can be retrieved are described in `t:setting/0`.
Any other atom passed as `name` will raise an error.
## Examples
Mint.HTTP2.get_server_setting(conn, :max_concurrent_streams)
#=> 500
"""
@spec get_server_setting(t(), atom()) :: term()
def get_server_setting(%__MODULE__{} = conn, name) when is_atom(name) do
get_setting(conn.server_settings, name)
end
@doc """
Gets the value of the given HTTP/2 client setting.
This function returns the value of the given HTTP/2 setting that the client
advertised to the server. Client settings can be advertised through `put_settings/2`
or when starting up a connection.
Client settings have to be acknowledged by the server before coming into effect.
This function is HTTP/2 specific. For more information on HTTP/2 settings, see
[the related section in the RFC](https://httpwg.org/specs/rfc7540.html#SettingValues).
See the "HTTP/2 settings" section in the module documentation for more information.
## Supported settings
The possible settings that can be retrieved are described in `t:setting/0`.
Any other atom passed as `name` will raise an error.
## Examples
Mint.HTTP2.get_client_setting(conn, :max_concurrent_streams)
#=> 500
"""
@spec get_client_setting(t(), atom()) :: term()
def get_client_setting(%__MODULE__{} = conn, name) when is_atom(name) do
get_setting(conn.client_settings, name)
end
defp get_setting(settings, name) do
case Map.fetch(settings, name) do
{:ok, value} -> value
:error -> raise ArgumentError, "unknown HTTP/2 setting: #{inspect(name)}"
end
end
@doc """
Cancels an in-flight request.
This function is HTTP/2 specific. It cancels an in-flight request. The server could have
already sent responses for the request you want to cancel: those responses will be parsed
by the connection but not returned to the user. No more responses
to a request will be returned after you call `cancel_request/2` on that request.
If there's no error in canceling the request, `{:ok, conn}` is returned where `conn` is
the updated connection. If there's an error, `{:error, conn, reason}` is returned where
`conn` is the updated connection and `reason` is the error reason.
## Examples
{:ok, conn, ref} = Mint.HTTP2.request(conn, "GET", "/", _headers = [])
{:ok, conn} = Mint.HTTP2.cancel_request(conn, ref)
"""
@spec cancel_request(t(), Types.request_ref()) :: {:ok, t()} | {:error, t(), Types.error()}
def cancel_request(%__MODULE__{} = conn, request_ref) when is_reference(request_ref) do
case Map.fetch(conn.ref_to_stream_id, request_ref) do
{:ok, stream_id} ->
conn = close_stream!(conn, stream_id, _error_code = :cancel)
{:ok, conn}
:error ->
{:ok, conn}
end
catch
:throw, {:mint, conn, error} -> {:error, conn, error}
end
@doc """
Returns the window size of the connection or of a single request.
This function is HTTP/2 specific. It returns the window size of
either the connection if `connection_or_request` is `:connection` or of a single
request if `connection_or_request` is `{:request, request_ref}`.
Use this function to check the window size of the connection before sending a
full request. Also use this function to check the window size of both the
connection and of a request if you want to stream body chunks on that request.
For more information on flow control and window sizes in HTTP/2, see the section
below.
## HTTP/2 Flow Control
In HTTP/2, flow control is implemented through a
window size. When the client sends data to the server, the window size is decreased
and the server needs to "refill" it on the client side. You don't need to take care of
the refilling of the client window as it happens behind the scenes in `stream/2`.
A window size is kept for the entire connection and all requests affect this window
size. A window size is also kept per request.
The only thing that affects the window size is the body of a request, regardless of
if it's a full request sent with `request/5` or body chunks sent through
`stream_request_body/3`. That means that if we make a request with a body that is
five bytes long, like `"hello"`, the window size of the connection and the window size
of that particular request will decrease by five bytes.
If we use all the window size before the server refills it, functions like
`request/5` will return an error.
## Examples
On the connection:
HTTP2.get_window_size(conn, :connection)
#=> 65_536
On a single streamed request:
{:ok, conn, request_ref} = HTTP2.request(conn, "GET", "/", [], :stream)
HTTP2.get_window_size(conn, {:request, request_ref})
#=> 65_536
{:ok, conn} = HTTP2.stream_request_body(conn, request_ref, "hello")
HTTP2.get_window_size(conn, {:request, request_ref})
#=> 65_531
"""
@spec get_window_size(t(), :connection | {:request, Types.request_ref()}) :: non_neg_integer()
def get_window_size(conn, connection_or_request)
def get_window_size(%__MODULE__{} = conn, :connection) do
conn.window_size
end
def get_window_size(%__MODULE__{} = conn, {:request, request_ref}) do
case Map.fetch(conn.ref_to_stream_id, request_ref) do
{:ok, stream_id} ->
conn.streams[stream_id].window_size
:error ->
raise ArgumentError,
"request with request reference #{inspect(request_ref)} was not found"
end
end
@doc """
See `Mint.HTTP.stream/2`.
"""
@impl true
@spec stream(t(), term()) ::
{:ok, t(), [Types.response()]}
| {:error, t(), Types.error(), [Types.response()]}
| :unknown
def stream(conn, message)
def stream(%__MODULE__{socket: socket} = conn, {tag, socket, reason})
when tag in [:tcp_error, :ssl_error] do
error = conn.transport.wrap_error(reason)
{:error, %{conn | state: :closed}, error, _responses = []}
end
def stream(%__MODULE__{socket: socket} = conn, {tag, socket})
when tag in [:tcp_closed, :ssl_closed] do
handle_closed(conn)
end
def stream(%__MODULE__{transport: transport, socket: socket} = conn, {tag, socket, data})
when tag in [:tcp, :ssl] do
case maybe_concat_and_handle_new_data(conn, data) do
{:ok, %{mode: mode, state: state} = conn, responses}
when mode == :active and state != :closed ->
case transport.setopts(socket, active: :once) do
:ok -> {:ok, conn, responses}
{:error, reason} -> {:error, put_in(conn.state, :closed), reason, responses}
end
other ->
other
end
catch
:throw, {:mint, conn, error, responses} -> {:error, conn, error, responses}
end
def stream(%__MODULE__{}, _message) do
:unknown
end
@doc """
See `Mint.HTTP.open_request_count/1`.
In HTTP/2, the number of open requests is the number of requests **opened by the client**
that have not yet received a `:done` response. It's important to note that only
requests opened by the client (with `request/5`) count towards the number of open
requests, as requests opened from the server with server pushes (see the "Server push"
section in the module documentation) are not considered open requests. We do this because
clients might need to know how many open requests there are because the server limits
the number of concurrent requests the client can open. To know how many requests the client
can open, see `get_server_setting/2` with the `:max_concurrent_streams` setting.
"""
@impl true
@spec open_request_count(t()) :: non_neg_integer()
def open_request_count(%__MODULE__{} = conn) do
conn.open_client_stream_count
end
@doc """
See `Mint.HTTP.recv/3`.
"""
@impl true
@spec recv(t(), non_neg_integer(), timeout()) ::
{:ok, t(), [Types.response()]}
| {:error, t(), Types.error(), [Types.response()]}
def recv(conn, byte_count, timeout)
def recv(%__MODULE__{mode: :passive} = conn, byte_count, timeout) do
case conn.transport.recv(conn.socket, byte_count, timeout) do
{:ok, data} ->
maybe_concat_and_handle_new_data(conn, data)
{:error, %TransportError{reason: :closed}} ->
handle_closed(conn)
{:error, error} ->
{:error, %{conn | state: :closed}, error, _responses = []}
end
catch
:throw, {:mint, conn, error, responses} -> {:error, conn, error, responses}
end
def recv(_conn, _byte_count, _timeout) do
raise ArgumentError,
"can't use recv/3 to synchronously receive data when the mode is :active. " <>
"Use Mint.HTTP.set_mode/2 to set the connection to passive mode"
end
@doc """
See `Mint.HTTP.set_mode/2`.
"""
@impl true
@spec set_mode(t(), :active | :passive) :: {:ok, t()} | {:error, Types.error()}
def set_mode(%__MODULE__{} = conn, mode) when mode in [:active, :passive] do
active =
case mode do
:active -> :once
:passive -> false
end
with :ok <- conn.transport.setopts(conn.socket, active: active) do
{:ok, put_in(conn.mode, mode)}
end
end
@doc """
See `Mint.HTTP.controlling_process/2`.
"""
@impl true
@spec controlling_process(t(), pid()) :: {:ok, t()} | {:error, Types.error()}
def controlling_process(%__MODULE__{} = conn, new_pid) when is_pid(new_pid) do
with :ok <- conn.transport.controlling_process(conn.socket, new_pid) do
{:ok, conn}
end
end
@doc """
See `Mint.HTTP.put_private/3`.
"""
@impl true
@spec put_private(t(), atom(), term()) :: t()
def put_private(%__MODULE__{private: private} = conn, key, value) when is_atom(key) do
%{conn | private: Map.put(private, key, value)}
end
@doc """
See `Mint.HTTP.get_private/3`.
"""
@impl true
@spec get_private(t(), atom(), term()) :: term()
def get_private(%__MODULE__{private: private} = _conn, key, default \\ nil) when is_atom(key) do
Map.get(private, key, default)
end
@doc """
See `Mint.HTTP.delete_private/2`.
"""
@impl true
@spec delete_private(t(), atom()) :: t()
def delete_private(%__MODULE__{private: private} = conn, key) when is_atom(key) do
%{conn | private: Map.delete(private, key)}
end
@doc """
See `Mint.HTTP.put_log/2`.
"""
@doc since: "1.5.0"
@impl true
@spec put_log(t(), boolean()) :: t()
def put_log(%__MODULE__{} = conn, log?) when is_boolean(log?) do
%{conn | log: log?}
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.5
# SETTINGS parameters are not negotiated. We keep client settings and server settings separate.
@doc false
@impl true
@spec initiate(
Types.scheme(),
Types.socket(),
String.t(),
:inet.port_number(),
keyword()
) :: {:ok, t()} | {:error, Types.error()}
def initiate(scheme, socket, hostname, port, opts) do
transport = Util.scheme_to_transport(scheme)
scheme_string = Atom.to_string(scheme)
mode = Keyword.get(opts, :mode, :active)
log? = Keyword.get(opts, :log, false)
client_settings_params = Keyword.get(opts, :client_settings, [])
validate_client_settings!(client_settings_params)
# If the port is the default for the scheme, don't add it to the :authority pseudo-header
authority =
if URI.default_port(scheme_string) == port do
hostname
else
"#{hostname}:#{port}"
end
unless mode in [:active, :passive] do
raise ArgumentError,
"the :mode option must be either :active or :passive, got: #{inspect(mode)}"
end
unless is_boolean(log?) do
raise ArgumentError,
"the :log option must be a boolean, got: #{inspect(log?)}"
end
conn = %__MODULE__{
hostname: hostname,
port: port,
authority: authority,
transport: Util.scheme_to_transport(scheme),
socket: socket,
mode: mode,
scheme: scheme_string,
state: :handshaking,
log: log?