Skip to content

Commit 1dd3601

Browse files
Optimize HTTP/2 request creation (#423)
When constructing the authority pseudo header we check if the conn's port is the default for the conn's scheme using `URI.default_port/1`. That corresponds to an ETS lookup into the `:elixir_config` table. It's relatively fast to read that information but the conn's port and scheme are static for the life of the conn, so we should determine this information once while initiating the conn (`Mint.HTTP2.initiate/5`). This change saves a small amount of time per request and becomes more valuable as the conn is re-used for more requests. Using a charlist is slightly faster than passing `"CONNECT"` as a binary.
1 parent 321c830 commit 1dd3601

File tree

2 files changed

+55
-30
lines changed

2 files changed

+55
-30
lines changed

lib/mint/http2.ex

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ defmodule Mint.HTTP2 do
170170
:hostname,
171171
:port,
172172
:scheme,
173+
:authority,
173174

174175
# Connection state (open, closed, and so on).
175176
:state,
@@ -974,10 +975,18 @@ defmodule Mint.HTTP2 do
974975
) :: {:ok, t()} | {:error, Types.error()}
975976
def initiate(scheme, socket, hostname, port, opts) do
976977
transport = scheme_to_transport(scheme)
978+
scheme_string = Atom.to_string(scheme)
977979
mode = Keyword.get(opts, :mode, :active)
978980
log? = Keyword.get(opts, :log, false)
979981
client_settings_params = Keyword.get(opts, :client_settings, [])
980982
validate_client_settings!(client_settings_params)
983+
# If the port is the default for the scheme, don't add it to the :authority pseudo-header
984+
authority =
985+
if URI.default_port(scheme_string) == port do
986+
hostname
987+
else
988+
"#{hostname}:#{port}"
989+
end
981990

982991
unless mode in [:active, :passive] do
983992
raise ArgumentError,
@@ -992,10 +1001,11 @@ defmodule Mint.HTTP2 do
9921001
conn = %__MODULE__{
9931002
hostname: hostname,
9941003
port: port,
1004+
authority: authority,
9951005
transport: scheme_to_transport(scheme),
9961006
socket: socket,
9971007
mode: mode,
998-
scheme: Atom.to_string(scheme),
1008+
scheme: scheme_string,
9991009
state: :handshaking,
10001010
log: log?
10011011
}
@@ -1355,23 +1365,37 @@ defmodule Mint.HTTP2 do
13551365
end
13561366

13571367
defp add_pseudo_headers(headers, conn, method, path) do
1358-
if String.upcase(method) == "CONNECT" do
1368+
if is_method?(method, ~c"CONNECT") do
13591369
[
13601370
{":method", method},
1361-
{":authority", authority_pseudo_header(conn.scheme, conn.port, conn.hostname)}
1371+
{":authority", conn.authority}
13621372
| headers
13631373
]
13641374
else
13651375
[
13661376
{":method", method},
13671377
{":path", path},
13681378
{":scheme", conn.scheme},
1369-
{":authority", authority_pseudo_header(conn.scheme, conn.port, conn.hostname)}
1379+
{":authority", conn.authority}
13701380
| headers
13711381
]
13721382
end
13731383
end
13741384

1385+
@spec is_method?(proposed :: binary(), method :: charlist()) :: boolean()
1386+
defp is_method?(<<>>, []), do: true
1387+
1388+
defp is_method?(<<char, rest_bin::binary>>, [char | rest_list]) do
1389+
is_method?(rest_bin, rest_list)
1390+
end
1391+
1392+
defp is_method?(<<lower_char, rest_bin::binary>>, [char | rest_list])
1393+
when lower_char >= ?a and lower_char <= ?z and lower_char - 32 == char do
1394+
is_method?(rest_bin, rest_list)
1395+
end
1396+
1397+
defp is_method?(_proposed, _method), do: false
1398+
13751399
defp sort_pseudo_headers_to_front(headers) do
13761400
Enum.sort_by(headers, fn {key, _value} ->
13771401
not String.starts_with?(key, ":")
@@ -1719,15 +1743,6 @@ defmodule Mint.HTTP2 do
17191743
end
17201744
end
17211745

1722-
# If the port is the default for the scheme, don't add it to the :authority pseudo-header
1723-
defp authority_pseudo_header(scheme, port, hostname) do
1724-
if URI.default_port(scheme) == port do
1725-
hostname
1726-
else
1727-
"#{hostname}:#{port}"
1728-
end
1729-
end
1730-
17311746
defp join_cookie_headers(headers) do
17321747
# If we have 0 or 1 Cookie headers, we just use the old list of headers.
17331748
case Enum.split_with(headers, fn {name, _value} -> Util.downcase_ascii(name) == "cookie" end) do

test/mint/http2/conn_test.exs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule Mint.HTTP2Test do
2222
@server_pdict_key {__MODULE__, :http2_test_server}
2323

2424
setup :start_server_async
25+
setup :maybe_change_default_scheme_port
2526
setup :start_connection
2627
setup :maybe_set_transport_mock
2728

@@ -841,29 +842,21 @@ defmodule Mint.HTTP2Test do
841842
assert HTTP2.open?(conn)
842843
end
843844

845+
@tag :with_overridden_default_port
844846
test ":authority pseudo-header does not include port if it is the scheme's default",
845847
%{conn: conn} do
846-
default_https_port = URI.default_port("https")
847-
848-
try do
849-
# Override default https port for this test
850-
URI.default_port("https", conn.port)
851-
852-
{conn, _ref} = open_request(conn)
848+
{conn, _ref} = open_request(conn)
853849

854-
assert_recv_frames [headers(hbf: hbf)]
850+
assert_recv_frames [headers(hbf: hbf)]
855851

856-
assert {":authority", authority} =
857-
hbf
858-
|> server_decode_headers()
859-
|> List.keyfind(":authority", 0)
852+
assert {":authority", authority} =
853+
hbf
854+
|> server_decode_headers()
855+
|> List.keyfind(":authority", 0)
860856

861-
assert authority == conn.hostname
857+
assert authority == conn.hostname
862858

863-
assert HTTP2.open?(conn)
864-
after
865-
URI.default_port("https", default_https_port)
866-
end
859+
assert HTTP2.open?(conn)
867860
end
868861

869862
test "when there's a request body, the content-length header is passed if not present",
@@ -2374,6 +2367,23 @@ defmodule Mint.HTTP2Test do
23742367
%{}
23752368
end
23762369

2370+
defp maybe_change_default_scheme_port(%{
2371+
server_port: server_port,
2372+
with_overridden_default_port: _
2373+
}) do
2374+
default_https_port = URI.default_port("https")
2375+
2376+
on_exit(fn -> URI.default_port("https", default_https_port) end)
2377+
2378+
:ok = URI.default_port("https", server_port)
2379+
2380+
%{}
2381+
end
2382+
2383+
defp maybe_change_default_scheme_port(_context) do
2384+
%{}
2385+
end
2386+
23772387
defp recv_next_frames(n) do
23782388
server = Process.get(@server_pdict_key)
23792389
TestServer.recv_next_frames(server, n)

0 commit comments

Comments
 (0)