Skip to content

Add :downcase_request_headers option to HTTP1.connect #399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 12, 2024
134 changes: 134 additions & 0 deletions lib/mint/core/headers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
defmodule Mint.Core.Headers do
@type canonical_header() ::
{original_name :: String.t(), canonical_name :: String.t(), value :: String.t()}
@type raw_header() :: {original_name :: String.t(), value :: String.t()}

@unallowed_trailer_headers MapSet.new([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I mentioned it on some other occasion: "trailer headers" feels like a misnomer. Quickly looking at RFC 7231 the naming didn't seem to be super explicit, but in the newer one that surpasses it, RFC 9110, it is: http has fields: header fields (colloquially "headers") and trailer fields (colloquially "trailers")

I only mention this as an offer to send a patch to use "trailers" or "trailer fields" naming internally. It's probably not worth it to go through deprecations if the trailer_headers name was used in public APIs so I wouldn't touch these.

Feel free to ignore, it's not a big deal in the grand scheme of things. :)

"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_headers([raw_header()]) :: [canonical_header()]
def from_raw_headers(headers) do
Enum.map(headers, fn {name, value} -> {name, String.downcase(name, :ascii), value} end)
end

@spec to_raw_headers([canonical_header()], boolean()) :: [raw_header()]
def to_raw_headers(headers, _case_sensitive_headers = true) do
Enum.map(headers, fn {name, _canonical_name, value} -> {name, value} end)
end

def to_raw_headers(headers, _case_sensitive_headers = false) do
Enum.map(headers, fn {_name, canonical_name, value} ->
{canonical_name, value}
end)
end

@spec find_header([canonical_header()], String.t()) :: {String.t(), String.t()} | nil
def find_header(headers, name) do
case List.keyfind(headers, name, 1) do
nil -> nil
{name, _canonical_name, value} -> {name, value}
end
end

@spec replace_header([canonical_header()], String.t(), String.t(), String.t()) ::
[canonical_header()]
def replace_header(headers, new_name, canonical_name, value) do
List.keyreplace(headers, canonical_name, 1, {new_name, canonical_name, value})
end

@spec has_header?([canonical_header()], String.t()) :: boolean()
def has_header?(headers, name) do
List.keymember?(headers, name, 1)
end

@spec put_new_header([canonical_header()], String.t(), String.t(), String.t() | nil) ::
[canonical_header()]
def put_new_header(headers, _name, _canonical_name, nil) do
headers
end

def put_new_header(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_header([canonical_header()], String.t(), String.t(), (-> String.t())) ::
[canonical_header()]
def put_new_header_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_header()]) :: String.t() | nil
def find_unallowed_trailer(headers) do
Enum.find_value(headers, fn
{raw_name, canonical_name, _value} ->
if canonical_name in @unallowed_trailer_headers do
raw_name
end
end)
end

@spec remove_unallowed_trailer([raw_header()]) :: [raw_header()]
def remove_unallowed_trailer(headers) do
Enum.reject(headers, fn {name, _value} -> name in @unallowed_trailer_headers end)
end

@spec lower_raw(String.t()) :: String.t()
def lower_raw(name) do
String.downcase(name, :ascii)
end

@spec lower_raws([raw_header()]) :: [raw_header()]
def lower_raws(headers) do
Enum.map(headers, fn {name, value} -> {lower_raw(name), value} end)
end
end
65 changes: 0 additions & 65 deletions lib/mint/core/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
6 changes: 2 additions & 4 deletions lib/mint/http.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
Expand Down
Loading