diff --git a/lib/mint/core/headers.ex b/lib/mint/core/headers.ex new file mode 100644 index 00000000..9d3a061c --- /dev/null +++ b/lib/mint/core/headers.ex @@ -0,0 +1,136 @@ +defmodule Mint.Core.Headers do + @moduledoc false + + @type canonical() :: + {original_name :: String.t(), canonical_name :: String.t(), value :: String.t()} + @type raw() :: {original_name :: String.t(), value :: String.t()} + + @unallowed_trailers MapSet.new([ + "content-encoding", + "content-length", + "content-range", + "content-type", + "trailer", + "transfer-encoding", + + # Control headers (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.1) + "cache-control", + "expect", + "host", + "max-forwards", + "pragma", + "range", + "te", + + # Conditionals (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.2) + "if-match", + "if-none-match", + "if-modified-since", + "if-unmodified-since", + "if-range", + + # Authentication/authorization (https://tools.ietf.org/html/rfc7235#section-5.3) + "authorization", + "proxy-authenticate", + "proxy-authorization", + "www-authenticate", + + # Cookie management (https://tools.ietf.org/html/rfc6265) + "cookie", + "set-cookie", + + # Control data (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.7.1) + "age", + "cache-control", + "expires", + "date", + "location", + "retry-after", + "vary", + "warning" + ]) + + @spec from_raw([raw()]) :: [canonical()] + def from_raw(headers) do + Enum.map(headers, fn {name, value} -> {name, lower_raw(name), value} end) + end + + @spec to_raw([canonical()], boolean()) :: [raw()] + def to_raw(headers, _case_sensitive = true) do + Enum.map(headers, fn {name, _canonical_name, value} -> {name, value} end) + end + + def to_raw(headers, _case_sensitive = false) do + Enum.map(headers, fn {_name, canonical_name, value} -> + {canonical_name, value} + end) + end + + @spec find([canonical()], String.t()) :: {String.t(), String.t()} | nil + def find(headers, name) do + case List.keyfind(headers, name, 1) do + nil -> nil + {name, _canonical_name, value} -> {name, value} + end + end + + @spec replace([canonical()], String.t(), String.t(), String.t()) :: + [canonical()] + def replace(headers, new_name, canonical_name, value) do + List.keyreplace(headers, canonical_name, 1, {new_name, canonical_name, value}) + end + + @spec has?([canonical()], String.t()) :: boolean() + def has?(headers, name) do + List.keymember?(headers, name, 1) + end + + @spec put_new([canonical()], String.t(), String.t(), String.t() | nil) :: + [canonical()] + def put_new(headers, _name, _canonical_name, nil) do + headers + end + + def put_new(headers, name, canonical_name, value) do + if List.keymember?(headers, canonical_name, 1) do + headers + else + [{name, canonical_name, value} | headers] + end + end + + @spec put_new([canonical()], String.t(), String.t(), (-> String.t())) :: + [canonical()] + def put_new_lazy(headers, name, canonical_name, fun) do + if List.keymember?(headers, canonical_name, 1) do + headers + else + [{name, canonical_name, fun.()} | headers] + end + end + + @spec find_unallowed_trailer([canonical()]) :: String.t() | nil + def find_unallowed_trailer(headers) do + Enum.find_value(headers, fn + {raw_name, canonical_name, _value} -> + if canonical_name in @unallowed_trailers do + raw_name + end + end) + end + + @spec remove_unallowed_trailer([raw()]) :: [raw()] + def remove_unallowed_trailer(headers) do + Enum.reject(headers, fn {name, _value} -> name in @unallowed_trailers end) + end + + @spec lower_raw(String.t()) :: String.t() + def lower_raw(name) do + String.downcase(name, :ascii) + end + + @spec lower_raws([raw()]) :: [raw()] + def lower_raws(headers) do + Enum.map(headers, fn {name, value} -> {lower_raw(name), value} end) + end +end diff --git a/lib/mint/core/util.ex b/lib/mint/core/util.ex index 39c4e7a2..83c1b96d 100644 --- a/lib/mint/core/util.ex +++ b/lib/mint/core/util.ex @@ -3,51 +3,6 @@ defmodule Mint.Core.Util do alias Mint.Types - @unallowed_trailer_headers MapSet.new([ - "content-encoding", - "content-length", - "content-range", - "content-type", - "trailer", - "transfer-encoding", - - # Control headers (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.1) - "cache-control", - "expect", - "host", - "max-forwards", - "pragma", - "range", - "te", - - # Conditionals (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.2) - "if-match", - "if-none-match", - "if-modified-since", - "if-unmodified-since", - "if-range", - - # Authentication/authorization (https://tools.ietf.org/html/rfc7235#section-5.3) - "authorization", - "proxy-authenticate", - "proxy-authorization", - "www-authenticate", - - # Cookie management (https://tools.ietf.org/html/rfc6265) - "cookie", - "set-cookie", - - # Control data (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.7.1) - "age", - "cache-control", - "expires", - "date", - "location", - "retry-after", - "vary", - "warning" - ]) - @spec hostname(keyword(), String.t()) :: String.t() def hostname(opts, address) when is_list(opts) do case Keyword.fetch(opts, :hostname) do @@ -113,24 +68,4 @@ defmodule Mint.Core.Util do @spec maybe_concat(binary(), binary()) :: binary() def maybe_concat(<<>>, data), do: data def maybe_concat(buffer, data) when is_binary(buffer), do: buffer <> data - - @spec lower_header_name(String.t()) :: String.t() - def lower_header_name(name) do - String.downcase(name, :ascii) - end - - @spec lower_header_keys(Types.headers()) :: Types.headers() - def lower_header_keys(headers) do - :lists.map(fn {name, value} -> {lower_header_name(name), value} end, headers) - end - - @spec find_unallowed_trailer_header(Types.headers()) :: {String.t(), String.t()} | nil - def find_unallowed_trailer_header(headers) do - Enum.find(headers, fn {name, _value} -> name in @unallowed_trailer_headers end) - end - - @spec remove_unallowed_trailer_headers(Types.headers()) :: Types.headers() - def remove_unallowed_trailer_headers(headers) do - Enum.reject(headers, fn {name, _value} -> name in @unallowed_trailer_headers end) - end end diff --git a/lib/mint/http.ex b/lib/mint/http.ex index 14789faf..2d00f0d1 100644 --- a/lib/mint/http.ex +++ b/lib/mint/http.ex @@ -121,10 +121,8 @@ defmodule Mint.HTTP do > gets logged by using the `Logger` API and Erlang's `:logger` module. """ - import Mint.Core.Util - alias Mint.{Types, TunnelProxy, UnsafeProxy} - alias Mint.Core.Transport + alias Mint.Core.{Transport, Util} @behaviour Mint.Core.Conn @@ -410,7 +408,7 @@ defmodule Mint.HTTP do def connect(scheme, address, port, opts \\ []) do case Keyword.fetch(opts, :proxy) do {:ok, {proxy_scheme, proxy_address, proxy_port, proxy_opts}} -> - case scheme_to_transport(scheme) do + case Util.scheme_to_transport(scheme) do Transport.TCP -> proxy = {proxy_scheme, proxy_address, proxy_port} host = {scheme, address, port} diff --git a/lib/mint/http1.ex b/lib/mint/http1.ex index 4462efde..628b661b 100644 --- a/lib/mint/http1.ex +++ b/lib/mint/http1.ex @@ -13,9 +13,8 @@ defmodule Mint.HTTP1 do how to use the data structure and client architecture, see `Mint`. """ - import Mint.Core.Util + alias Mint.Core.{Headers, Util} - alias Mint.Core.Util alias Mint.HTTP1.{Parse, Request, Response} alias Mint.{HTTPError, TransportError, Types} @@ -93,6 +92,7 @@ defmodule Mint.HTTP1 do :transport, :mode, :scheme_as_string, + :case_sensitive_headers, requests: :queue.new(), state: :closed, buffer: "", @@ -117,6 +117,13 @@ defmodule Mint.HTTP1 do Same as `Mint.HTTP.connect/4`, but forces an HTTP/1 or HTTP/1.1 connection. This function doesn't support proxying. + + ## Additional Options + + * `:case_sensitive_headers` - (boolean) if set to `true` the case of the supplied + headers in requests will be preserved. The default is to lowercase the headers + because HTTP/1.1 header names are case-insensitive. + """ @spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) :: {:ok, t()} | {:error, Types.error()} @@ -124,7 +131,7 @@ defmodule Mint.HTTP1 do # TODO: Also ALPN negotiate HTTP1? hostname = Mint.Core.Util.hostname(opts, address) - transport = scheme_to_transport(scheme) + transport = Util.scheme_to_transport(scheme) transport_opts = Keyword.get(opts, :transport_opts, []) @@ -147,7 +154,7 @@ defmodule Mint.HTTP1 do def upgrade(old_scheme, socket, new_scheme, hostname, port, opts) do # TODO: Also ALPN negotiate HTTP1? - transport = scheme_to_transport(new_scheme) + transport = Util.scheme_to_transport(new_scheme) transport_opts = Keyword.get(opts, :transport_opts, []) @@ -168,7 +175,7 @@ defmodule Mint.HTTP1 do keyword() ) :: {:ok, t()} | {:error, Types.error()} def initiate(scheme, socket, hostname, port, opts) do - transport = scheme_to_transport(scheme) + transport = Util.scheme_to_transport(scheme) mode = Keyword.get(opts, :mode, :active) log? = Keyword.get(opts, :log, false) @@ -182,7 +189,7 @@ defmodule Mint.HTTP1 do "the :log option must be a boolean, got: #{inspect(log?)}" end - with :ok <- inet_opts(transport, socket), + with :ok <- Util.inet_opts(transport, socket), :ok <- if(mode == :active, do: transport.setopts(socket, active: :once), else: :ok) do conn = %__MODULE__{ transport: transport, @@ -192,7 +199,8 @@ defmodule Mint.HTTP1 do port: port, scheme_as_string: Atom.to_string(scheme), state: :open, - log: log? + log: log?, + case_sensitive_headers: Keyword.get(opts, :case_sensitive_headers, false) } {:ok, conn} @@ -262,11 +270,17 @@ defmodule Mint.HTTP1 do headers = headers - |> lower_header_keys() + |> Headers.from_raw() |> add_default_headers(conn) with {:ok, headers, encoding} <- add_content_length_or_transfer_encoding(headers, body), - {:ok, iodata} <- Request.encode(method, path, headers, body), + {:ok, iodata} <- + Request.encode( + method, + path, + Headers.to_raw(headers, conn.case_sensitive_headers), + body + ), :ok <- transport.send(socket, iodata) do request_ref = make_ref() request = new_request(request_ref, method, body, encoding) @@ -363,7 +377,7 @@ defmodule Mint.HTTP1 do ref, chunk ) do - with {:ok, chunk} <- validate_chunk(chunk), + with {:ok, chunk} <- validate_chunk(conn, chunk), :ok <- conn.transport.send(conn.socket, Request.encode_chunk(chunk)) do case chunk do :eof -> @@ -391,21 +405,21 @@ defmodule Mint.HTTP1 do end end - defp validate_chunk({:eof, trailer_headers}) do - headers = lower_header_keys(trailer_headers) + defp validate_chunk(conn, {:eof, trailers}) do + trailers = Headers.from_raw(trailers) - if unallowed_header = find_unallowed_trailer_header(headers) do + if unallowed_header = Headers.find_unallowed_trailer(trailers) do {:error, wrap_error({:unallowed_trailing_header, unallowed_header})} else - {:ok, {:eof, headers}} + {:ok, {:eof, Headers.to_raw(trailers, conn.case_sensitive_headers)}} end end - defp validate_chunk(:eof) do + defp validate_chunk(_conn, :eof) do {:ok, :eof} end - defp validate_chunk(chunk) do + defp validate_chunk(_conn, chunk) do if IO.iodata_length(chunk) == 0 do :empty_chunk else @@ -458,7 +472,7 @@ defmodule Mint.HTTP1 do end defp handle_data(%__MODULE__{request: request} = conn, data) do - data = maybe_concat(conn.buffer, data) + data = Util.maybe_concat(conn.buffer, data) case decode(request.state, conn, data, []) do {:ok, conn, responses} -> @@ -795,7 +809,7 @@ defmodule Mint.HTTP1 do decode_trailer_headers(conn, rest, responses, headers) {:ok, :eof, rest} -> - headers = Util.remove_unallowed_trailer_headers(headers) + headers = Headers.remove_unallowed_trailer(headers) responses = [ {:done, conn.request.ref} @@ -974,8 +988,8 @@ defmodule Mint.HTTP1 do defp add_default_headers(headers, conn) do headers - |> Util.put_new_header("user-agent", @user_agent) - |> Util.put_new_header("host", default_host_header(conn)) + |> Headers.put_new("User-Agent", "user-agent", @user_agent) + |> Headers.put_new("Host", "host", default_host_header(conn)) end # If the port is the default for the scheme, don't add it to the host header @@ -989,18 +1003,19 @@ defmodule Mint.HTTP1 do defp add_content_length_or_transfer_encoding(headers, :stream) do cond do - List.keymember?(headers, "content-length", 0) -> + Headers.has?(headers, "content-length") -> {:ok, headers, :identity} - found = List.keyfind(headers, "transfer-encoding", 0) -> - {"transfer-encoding", value} = found + found = Headers.find(headers, "transfer-encoding") -> + {raw_name, value} = found with {:ok, tokens} <- Parse.transfer_encoding_header(value) do if "chunked" in tokens or "identity" in tokens do {:ok, headers, :identity} else - new_transfer_encoding = {"transfer-encoding", value <> ",chunked"} - headers = List.keyreplace(headers, "transfer-encoding", 0, new_transfer_encoding) + headers = + Headers.replace(headers, raw_name, "transfer-encoding", value <> ",chunked") + {:ok, headers, :chunked} end end @@ -1008,7 +1023,9 @@ defmodule Mint.HTTP1 do # If no content-length or transfer-encoding are present, assume # chunked transfer-encoding and handle the encoding ourselves. true -> - headers = Util.put_new_header(headers, "transfer-encoding", "chunked") + headers = + Headers.put_new(headers, "Transfer-Encoding", "transfer-encoding", "chunked") + {:ok, headers, :chunked} end end @@ -1019,7 +1036,9 @@ defmodule Mint.HTTP1 do defp add_content_length_or_transfer_encoding(headers, body) do length_fun = fn -> body |> IO.iodata_length() |> Integer.to_string() end - {:ok, Util.put_new_header_lazy(headers, "content-length", length_fun), :identity} + + {:ok, Headers.put_new_lazy(headers, "Content-Length", "content-length", length_fun), + :identity} end defp wrap_error(reason) do diff --git a/lib/mint/http1/response.ex b/lib/mint/http1/response.ex index e747e01a..a2018c40 100644 --- a/lib/mint/http1/response.ex +++ b/lib/mint/http1/response.ex @@ -1,6 +1,8 @@ defmodule Mint.HTTP1.Response do @moduledoc false + alias Mint.Core.Headers + def decode_status_line(binary) do case :erlang.decode_packet(:http_bin, binary, []) do {:ok, {:http_response, version, status, reason}, rest} -> @@ -37,5 +39,5 @@ defmodule Mint.HTTP1.Response do end defp header_name(atom) when is_atom(atom), do: atom |> Atom.to_string() |> header_name() - defp header_name(binary) when is_binary(binary), do: Mint.Core.Util.lower_header_name(binary) + defp header_name(binary) when is_binary(binary), do: Headers.lower_raw(binary) end diff --git a/lib/mint/http2.ex b/lib/mint/http2.ex index e9e67d61..9beaef0a 100644 --- a/lib/mint/http2.ex +++ b/lib/mint/http2.ex @@ -125,12 +125,11 @@ defmodule Mint.HTTP2 do > is enabled. """ - import Mint.Core.Util import Mint.HTTP2.Frame, except: [encode: 1, decode_next: 1, inspect: 1] alias Mint.{HTTPError, TransportError} alias Mint.Types - alias Mint.Core.Util + alias Mint.Core.{Headers, Util} alias Mint.HTTP2.Frame require Logger @@ -405,7 +404,7 @@ defmodule Mint.HTTP2 do keyword() ) :: {:ok, t()} | {:error, Types.error()} def upgrade(old_scheme, socket, new_scheme, hostname, port, opts) do - transport = scheme_to_transport(new_scheme) + transport = Util.scheme_to_transport(new_scheme) transport_opts = opts @@ -521,7 +520,7 @@ defmodule Mint.HTTP2 do when is_binary(method) and is_binary(path) and is_list(headers) do headers = headers - |> lower_header_keys() + |> Headers.lower_raws() |> add_pseudo_headers(conn, method, path) |> add_default_headers(body) |> sort_pseudo_headers_to_front() @@ -974,7 +973,7 @@ defmodule Mint.HTTP2 do keyword() ) :: {:ok, t()} | {:error, Types.error()} def initiate(scheme, socket, hostname, port, opts) do - transport = scheme_to_transport(scheme) + transport = Util.scheme_to_transport(scheme) scheme_string = Atom.to_string(scheme) mode = Keyword.get(opts, :mode, :active) log? = Keyword.get(opts, :log, false) @@ -1002,7 +1001,7 @@ defmodule Mint.HTTP2 do hostname: hostname, port: port, authority: authority, - transport: scheme_to_transport(scheme), + transport: Util.scheme_to_transport(scheme), socket: socket, mode: mode, scheme: scheme_string, @@ -1010,7 +1009,7 @@ defmodule Mint.HTTP2 do log: log? } - with :ok <- inet_opts(transport, socket), + with :ok <- Util.inet_opts(transport, socket), client_settings = settings(stream_id: 0, params: client_settings_params), preface = [@connection_preface, Frame.encode(client_settings)], :ok <- transport.send(socket, preface), @@ -1065,12 +1064,12 @@ defmodule Mint.HTTP2 do defp negotiate(address, port, :http, transport_opts) do # We don't support protocol negotiation for TCP connections # so currently we just assume the HTTP/2 protocol - transport = scheme_to_transport(:http) + transport = Util.scheme_to_transport(:http) transport.connect(address, port, transport_opts) end defp negotiate(address, port, :https, transport_opts) do - transport = scheme_to_transport(:https) + transport = Util.scheme_to_transport(:https) with {:ok, socket} <- transport.connect(address, port, transport_opts), {:ok, protocol} <- transport.negotiated_protocol(socket) do @@ -1107,14 +1106,15 @@ defmodule Mint.HTTP2 do encode_data(conn, stream_id, "", [:end_stream]) end - defp encode_stream_body_request_payload(conn, stream_id, {:eof, trailer_headers}) do - lowered_headers = lower_header_keys(trailer_headers) + defp encode_stream_body_request_payload(conn, stream_id, {:eof, trailers}) do + trailers = Headers.from_raw(trailers) - if unallowed_trailer_header = Util.find_unallowed_trailer_header(lowered_headers) do + if unallowed_trailer_header = Headers.find_unallowed_trailer(trailers) do error = wrap_error({:unallowed_trailing_header, unallowed_trailer_header}) throw({:mint, conn, error}) end + trailer_headers = Headers.to_raw(trailers, _case_sensitive = false) encode_headers(conn, stream_id, trailer_headers, [:end_headers, :end_stream]) end @@ -1402,7 +1402,7 @@ defmodule Mint.HTTP2 do ## Frame handling defp maybe_concat_and_handle_new_data(conn, data) do - data = maybe_concat(conn.buffer, data) + data = Util.maybe_concat(conn.buffer, data) {conn, responses} = handle_new_data(conn, data, []) {:ok, conn, Enum.reverse(responses)} end @@ -1705,7 +1705,7 @@ defmodule Mint.HTTP2 do headers when received_first_headers? -> if end_stream? do conn = close_stream!(conn, stream.id, :no_error) - headers = headers |> Util.remove_unallowed_trailer_headers() |> join_cookie_headers() + headers = headers |> Headers.remove_unallowed_trailer() |> join_cookie_headers() {conn, [{:done, ref}, {:headers, ref, headers} | responses]} else # Trailer headers must set the END_STREAM flag because they're @@ -1742,7 +1742,7 @@ defmodule Mint.HTTP2 do defp join_cookie_headers(headers) do # If we have 0 or 1 Cookie headers, we just use the old list of headers. - case Enum.split_with(headers, fn {name, _value} -> lower_header_name(name) == "cookie" end) do + case Enum.split_with(headers, fn {name, _value} -> Headers.lower_raw(name) == "cookie" end) do {[], _headers} -> headers @@ -2227,8 +2227,8 @@ defmodule Mint.HTTP2 do "can't send more data on this request since it's not streaming" end - def format_error({:unallowed_trailing_header, {name, value}}) do - "header #{inspect(name)} (with value #{inspect(value)}) is not allowed as a trailer header" + def format_error({:unallowed_trailing_header, name}) do + "header #{inspect(name)} is not allowed as a trailer header" end def format_error(:missing_status_header) do diff --git a/lib/mint/negotiate.ex b/lib/mint/negotiate.ex index c5fcfd5e..a059b04d 100644 --- a/lib/mint/negotiate.ex +++ b/lib/mint/negotiate.ex @@ -1,8 +1,6 @@ defmodule Mint.Negotiate do @moduledoc false - import Mint.Core.Util - alias Mint.{ HTTP1, HTTP2, @@ -10,6 +8,8 @@ defmodule Mint.Negotiate do Types } + alias Mint.Core.Util + @default_protocols [:http1, :http2] @transport_opts [alpn_advertised_protocols: ["http/1.1", "h2"]] @@ -68,7 +68,7 @@ defmodule Mint.Negotiate do end defp connect_negotiate(scheme, address, port, opts) do - transport = scheme_to_transport(scheme) + transport = Util.scheme_to_transport(scheme) hostname = Mint.Core.Util.hostname(opts, address) transport_opts = @@ -106,7 +106,7 @@ defmodule Mint.Negotiate do end defp connect_upgrade(proxy_scheme, transport_state, new_scheme, hostname, port, opts) do - transport = scheme_to_transport(new_scheme) + transport = Util.scheme_to_transport(new_scheme) transport_opts = opts @@ -123,7 +123,7 @@ defmodule Mint.Negotiate do end defp alpn_negotiate(scheme, socket, hostname, port, opts) do - transport = scheme_to_transport(scheme) + transport = Util.scheme_to_transport(scheme) case transport.negotiated_protocol(socket) do {:ok, "http/1.1"} -> diff --git a/test/mint/http1/conn_test.exs b/test/mint/http1/conn_test.exs index 2398455e..779be260 100644 --- a/test/mint/http1/conn_test.exs +++ b/test/mint/http1/conn_test.exs @@ -650,6 +650,116 @@ defmodule Mint.HTTP1Test do assert HTTP1.open?(conn) end + + test "non lower case headers", %{port: port, server_ref: server_ref} do + assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, case_sensitive_headers: true) + + assert_receive {^server_ref, server_socket} + body = "body" + content_length = byte_size(body) |> Integer.to_string() + + {:ok, _conn, _ref} = + HTTP1.request( + conn, + "GET", + "/", + [ + {"User-Agent", "myapp/1.0"}, + {"Host", "localhost"}, + {"Content-Length", content_length} + ], + body + ) + + assert receive_request_string(server_socket) == + request_string(""" + GET / HTTP/1.1 + User-Agent: myapp/1.0 + Host: localhost + Content-Length: 4 + + body\ + """) + end + + test "non lower case headers using defaults", %{port: port, server_ref: server_ref} do + assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, case_sensitive_headers: true) + + assert_receive {^server_ref, server_socket} + body = "body" + + {:ok, _conn, _ref} = + HTTP1.request( + conn, + "GET", + "/", + [ + {"User-Agent", "myapp/1.0"} + ], + body + ) + + assert receive_request_string(server_socket) == + request_string(""" + GET / HTTP/1.1 + Content-Length: 4 + Host: localhost:#{port} + User-Agent: myapp/1.0 + + body\ + """) + end + + test "non lower case headers identity transfer encoding", %{ + port: port, + server_ref: server_ref + } do + assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, case_sensitive_headers: true) + + assert_receive {^server_ref, server_socket} + + {:ok, _conn, _ref} = + HTTP1.request( + conn, + "GET", + "/", + [{"User-Agent", "myapp/1.0"}, {"Host", "localhost"}, {"TRANSFER-ENCODING", "identity"}], + :stream + ) + + assert receive_request_string(server_socket) == + request_string(""" + GET / HTTP/1.1 + User-Agent: myapp/1.0 + Host: localhost + TRANSFER-ENCODING: identity + + """) + end + + test "non lower case headers gzip encoding", %{port: port, server_ref: server_ref} do + assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, case_sensitive_headers: true) + + assert_receive {^server_ref, server_socket} + + {:ok, _conn, _ref} = + HTTP1.request( + conn, + "GET", + "/", + [{"User-Agent", "myapp/1.0"}, {"Host", "localhost"}, {"TRANSFER-ENCODING", "gzip"}], + :stream + ) + + assert receive_request_string(server_socket) == + request_string(""" + GET / HTTP/1.1 + User-Agent: myapp/1.0 + Host: localhost + TRANSFER-ENCODING: gzip,chunked + + """) + end end describe "streaming requests" do @@ -827,7 +937,7 @@ defmodule Mint.HTTP1Test do assert {:error, _conn, error} = HTTP1.stream_request_body(conn, ref, {:eof, trailer_headers}) - assert %HTTPError{reason: {:unallowed_trailing_header, {"host", "example.com"}}} = error + assert %HTTPError{reason: {:unallowed_trailing_header, "Host"}} = error end test "pipeline", %{conn: conn} do diff --git a/test/mint/http2/conn_test.exs b/test/mint/http2/conn_test.exs index 08fc869c..429d3000 100644 --- a/test/mint/http2/conn_test.exs +++ b/test/mint/http2/conn_test.exs @@ -1980,7 +1980,7 @@ defmodule Mint.HTTP2Test do assert {:error, %HTTP2{} = _conn, error} = HTTP2.stream_request_body(conn, ref, {:eof, trailer_headers}) - assert_http2_error error, {:unallowed_trailing_header, {"host", "example.com"}} + assert_http2_error error, {:unallowed_trailing_header, "Host"} assert HTTP2.get_window_size(conn, :connection) == expected_window_size assert HTTP2.open_request_count(conn) == 1