Skip to content

Commit

Permalink
Ensure headers are downcased (#227)
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach authored Aug 25, 2023
1 parent 315d354 commit b4fa02c
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 47 deletions.
23 changes: 16 additions & 7 deletions lib/req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,11 @@ defmodule Req do
* atom header names are turned into strings, replacing `_` with `-`. For example,
`:user_agent` becomes `"user-agent"`.
* string header names are left as is. Because header keys are case-insensitive
in both HTTP/1.1 and HTTP/2, it is recommended for header keys to be in
lowercase, to avoid sending duplicate keys in a request.
* string header names are downcased.
* `DateTime` header values are encoded as "HTTP date". Otherwise,
the header value is encoded with `String.Chars.to_string/1`.
* `%DateTime{}` header values are encoded as "HTTP date".
* other header values are encoded with `String.Chars.to_string/1`.
If you set `:headers` options both in `Req.new/1` and `request/2`, the header lists are merged.
Expand Down Expand Up @@ -948,10 +947,10 @@ defmodule Req do
name =
case name do
atom when is_atom(atom) ->
atom |> Atom.to_string() |> String.replace("_", "-")
atom |> Atom.to_string() |> String.replace("_", "-") |> String.downcase(:ascii)

binary when is_binary(binary) ->
binary
String.downcase(binary, :ascii)
end

value =
Expand Down Expand Up @@ -998,4 +997,14 @@ defmodule Req do
body: Keyword.get(options, :body, "")
}
end

def __ensure_header_downcase__(name) do
downcased = String.downcase(name, :ascii)

if name != downcased do
IO.warn("header names should be downcased, got: #{name}")
end

downcased
end
end
47 changes: 21 additions & 26 deletions lib/req/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ defmodule Req.Request do
Public fields are:
* `:method` - the HTTP request method
* `:method` - the HTTP request method.
* `:url` - the HTTP request URL
* `:url` - the HTTP request URL.
* `:headers` - the HTTP request headers
* `:headers` - the HTTP request headers. The header names must be downcased.
* `:body` - the HTTP request body
* `:body` - the HTTP request body.
Can be one of:
Expand Down Expand Up @@ -304,7 +304,7 @@ defmodule Req.Request do
[:with_body]
) do
{:ok, status, headers, body} ->
headers = for {name, value} <- headers, do: {String.downcase(name), value}
headers = for {name, value} <- headers, do: {String.downcase(name, :ascii), value}
response = %Req.Response{status: status, headers: headers, body: body}
{request, response}
Expand Down Expand Up @@ -647,7 +647,7 @@ defmodule Req.Request do
end

@doc """
Returns the values of the header specified by `key`.
Returns the values of the header specified by `name`.
## Examples
Expand All @@ -657,21 +657,15 @@ defmodule Req.Request do
"""
@spec get_header(t(), binary()) :: [binary()]
def get_header(%Req.Request{} = request, key) when is_binary(key) do
for {^key, value} <- request.headers, do: value
def get_header(%Req.Request{} = request, name) when is_binary(name) do
name = Req.__ensure_header_downcase__(name)
for {^name, value} <- request.headers, do: value
end

@doc """
Adds a new request header (`key`) if not present, otherwise replaces the
Adds a new request header `name` if not present, otherwise replaces the
previous value of that header with `value`.
Because header keys are case-insensitive in both HTTP/1.1 and HTTP/2,
it is recommended for header keys to be in lowercase, to avoid sending
duplicate keys in a request.
Additionally, requests with mixed-case headers served over HTTP/2 are not
considered valid by common clients, resulting in dropped requests.
## Examples
iex> req = Req.new()
Expand All @@ -681,9 +675,10 @@ defmodule Req.Request do
"""
@spec put_header(t(), binary(), binary()) :: t()
def put_header(%Req.Request{} = request, key, value)
when is_binary(key) and is_binary(value) do
%{request | headers: List.keystore(request.headers, key, 0, {key, value})}
def put_header(%Req.Request{} = request, name, value)
when is_binary(name) and is_binary(value) do
name = Req.__ensure_header_downcase__(name)
%{request | headers: List.keystore(request.headers, name, 0, {name, value})}
end

@doc """
Expand All @@ -700,13 +695,13 @@ defmodule Req.Request do
"""
@spec put_headers(t(), [{binary(), binary()}]) :: t()
def put_headers(%Req.Request{} = request, headers) do
for {key, value} <- headers, reduce: request do
acc -> put_header(acc, key, value)
for {name, value} <- headers, reduce: request do
acc -> put_header(acc, name, value)
end
end

@doc """
Adds a request header (`key`) unless already present.
Adds a request header `name` unless already present.
See `put_header/3` for more information.
Expand All @@ -720,11 +715,11 @@ defmodule Req.Request do
[{"accept", "application/json"}]
"""
@spec put_new_header(t(), binary(), binary()) :: t()
def put_new_header(%Req.Request{} = request, key, value)
when is_binary(key) and is_binary(value) do
case get_header(request, key) do
def put_new_header(%Req.Request{} = request, name, value)
when is_binary(name) and is_binary(value) do
case get_header(request, name) do
[] ->
put_header(request, key, value)
put_header(request, name, value)

_ ->
request
Expand Down
32 changes: 18 additions & 14 deletions lib/req/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ defmodule Req.Response do
Fields:
* `:status` - the HTTP status code
* `:status` - the HTTP status code.
* `:headers` - the HTTP response headers
* `:headers` - the HTTP response headers. The header names must be downcased.
* `:body` - the HTTP response body
* `:body` - the HTTP response body.
* `:private` - a map reserved for libraries and frameworks to use.
Prefix the keys with the name of your project to avoid any future
Expand Down Expand Up @@ -114,32 +114,34 @@ defmodule Req.Response do
end

@doc """
Returns the values of the header specified by `key`.
Returns the values of the header specified by `name`.
## Examples
iex> Req.Response.get_header(response, "content-type")
["application/json"]
"""
@spec get_header(t(), binary()) :: [binary()]
def get_header(%Req.Response{} = response, key) when is_binary(key) do
for {^key, value} <- response.headers, do: value
def get_header(%Req.Response{} = response, name) when is_binary(name) do
name = Req.__ensure_header_downcase__(name)
for {^name, value} <- response.headers, do: value
end

@doc """
Adds a new response header (`key`) if not present, otherwise replaces the
Adds a new response header `name` if not present, otherwise replaces the
previous value of that header with `value`.
## Examples
iex> Req.Response.put_header(response, "content-type", "application/json").headers
iex> resp = Req.Response.put_header(resp, "content-type", "application/json")
iex> resp.headers
[{"content-type", "application/json"}]
"""
@spec put_header(t(), binary(), binary()) :: t()
def put_header(%Req.Response{} = response, key, value)
when is_binary(key) and is_binary(value) do
%{response | headers: List.keystore(response.headers, key, 0, {key, value})}
def put_header(%Req.Response{} = response, name, value)
when is_binary(name) and is_binary(value) do
name = Req.__ensure_header_downcase__(name)
%{response | headers: List.keystore(response.headers, name, 0, {name, value})}
end

@doc """
Expand All @@ -156,13 +158,15 @@ defmodule Req.Response do
[]
"""
def delete_header(%Req.Response{} = response, key) when is_binary(key) do
def delete_header(%Req.Response{} = response, name) when is_binary(name) do
name_to_delete = Req.__ensure_header_downcase__(name)

%Req.Response{
response
| headers:
for(
{name, value} <- response.headers,
String.downcase(name) != String.downcase(key),
name != name_to_delete,
do: {name, value}
)
}
Expand Down

0 comments on commit b4fa02c

Please sign in to comment.