Skip to content

Commit

Permalink
Req.Request, Req.Response: Change headers to be maps
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach committed Aug 24, 2023
1 parent 709b424 commit 6ce83c9
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 154 deletions.
70 changes: 40 additions & 30 deletions lib/req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -336,14 +336,14 @@ defmodule Req do
iex> req = Req.new(headers: [point_x: 1])
iex> req = Req.update(req, headers: [point_y: 2])
iex> req.headers
[{"point-x", "1"}, {"point-y", "2"}]
%{"point-x" => ["1"], "point-y" => ["2"]}
The same header names are overwritten however:
iex> req = Req.new(headers: [authorization: "bearer foo"])
iex> req = Req.update(req, headers: [authorization: "bearer bar"])
iex> req.headers
[{"authorization", "bearer bar"}]
%{"authorization" => ["bearer bar"]}
Similarly to headers, `:params` are merged too:
Expand Down Expand Up @@ -373,9 +373,7 @@ defmodule Req do

{:headers, new_headers}, acc ->
update_in(acc.headers, fn old_headers ->
new_headers = encode_headers(new_headers)
new_header_names = Enum.map(new_headers, &elem(&1, 0))
Enum.reject(old_headers, &(elem(&1, 0) in new_header_names)) ++ new_headers
Map.merge(old_headers, encode_headers(new_headers))
end)

{name, value}, acc ->
Expand Down Expand Up @@ -944,31 +942,43 @@ defmodule Req do
end

defp encode_headers(headers) do
for {name, value} <- headers do
name =
case name do
atom when is_atom(atom) ->
atom |> Atom.to_string() |> String.replace("_", "-")

binary when is_binary(binary) ->
binary
end

value =
case value do
%DateTime{} = datetime ->
datetime |> DateTime.shift_zone!("Etc/UTC") |> Req.Steps.format_http_datetime()

%NaiveDateTime{} = datetime ->
IO.warn("setting header to %NaiveDateTime{} is deprecated, use %DateTime{} instead")
Req.Steps.format_http_datetime(datetime)

_ ->
String.Chars.to_string(value)
end

{name, value}
end
Enum.reduce(headers, %{}, fn {name, value}, acc ->
Map.update(
acc,
encode_header_name(name),
encode_header_values(List.wrap(value)),
&(&1 ++ encode_header_values(List.wrap(value)))
)
end)
end

defp encode_header_values([value | rest]) do
[encode_header_value(value) | encode_header_values(rest)]
end

defp encode_header_values([]) do
[]
end

defp encode_header_name(name) when is_atom(name) do
name |> Atom.to_string() |> String.replace("_", "-")
end

defp encode_header_name(name) when is_binary(name) do
name
end

defp encode_header_value(%DateTime{} = datetime) do
datetime |> DateTime.shift_zone!("Etc/UTC") |> Req.Steps.format_http_datetime()
end

defp encode_header_value(%NaiveDateTime{} = datetime) do
IO.warn("setting header to %NaiveDateTime{} is deprecated, use %DateTime{} instead")
Req.Steps.format_http_datetime(datetime)
end

defp encode_header_value(value) do
String.Chars.to_string(value)
end

# Plugins support is experimental, undocumented, and likely won't make the new release.
Expand Down
67 changes: 39 additions & 28 deletions lib/req/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ defmodule Req.Request do
@type t() :: %Req.Request{
method: atom(),
url: URI.t(),
headers: [{binary(), binary()}],
headers: %{binary() => [binary()]},
body: iodata() | {:stream, Enumerable.t()} | nil,
options: options(),
halted: boolean(),
Expand All @@ -338,7 +338,7 @@ defmodule Req.Request do

defstruct method: :get,
url: URI.parse(""),
headers: [],
headers: %{},
body: nil,
options: %{},
halted: false,
Expand Down Expand Up @@ -379,6 +379,11 @@ defmodule Req.Request do
options
|> Keyword.validate!([:method, :url, :headers, :body, :adapter, :options])
|> Keyword.update(:url, URI.new!(""), &URI.new!/1)
|> Keyword.update(:headers, %{}, fn headers ->
Map.new(headers, fn {key, value} ->
{key, List.wrap(value)}
end)
end)
|> Keyword.update(:options, %{}, &Map.new/1)

struct!(__MODULE__, options)
Expand Down Expand Up @@ -654,36 +659,41 @@ defmodule Req.Request do
iex> req = Req.new(headers: [{"accept", "application/json"}])
iex> Req.Request.get_header(req, "accept")
["application/json"]
iex> Req.Request.get_header(req, "x-unknown")
[]
"""
@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
Map.get(request.headers, key, [])
end

@doc """
Adds a new request header (`key`) if not present, otherwise replaces the
previous value of that header with `value`.
Sets the header `key` to `value`.
The value can be a binary or a list of binaries,
If the header was previously set, its value is overwritten.
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.
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()
iex> req = Req.Request.put_header(req, "accept", "application/json")
iex> req.headers
[{"accept", "application/json"}]
%{"accept" => ["application/json"]}
"""
@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})}
when is_binary(key) and
(is_binary(value) or is_list(value)) do
put_in(request.headers[key], List.wrap(value))
end

@doc """
Expand All @@ -696,7 +706,7 @@ defmodule Req.Request do
iex> req = Req.new()
iex> req = Req.Request.put_headers(req, [{"accept", "text/html"}, {"accept-encoding", "gzip"}])
iex> req.headers
[{"accept", "text/html"}, {"accept-encoding", "gzip"}]
%{"accept" => ["text/html"], "accept-encoding" => ["gzip"]}
"""
@spec put_headers(t(), [{binary(), binary()}]) :: t()
def put_headers(%Req.Request{} = request, headers) do
Expand All @@ -717,18 +727,12 @@ defmodule Req.Request do
...> |> Req.Request.put_new_header("accept", "application/json")
...> |> Req.Request.put_new_header("accept", "application/html")
iex> req.headers
[{"accept", "application/json"}]
%{"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
[] ->
put_header(request, key, value)

_ ->
request
end
when is_binary(key) and (is_binary(value) or is_list(value)) do
update_in(request.headers, &Map.put_new(&1, key, List.wrap(value)))
end

@doc """
Expand Down Expand Up @@ -910,12 +914,19 @@ defmodule Req.Request do
{headers, options} =
if Req.Request.get_option(request, :redact_auth, true) do
headers =
for {name, value} <- request.headers do
if name in ["authorization", "Authorization"] do
{name, "[redacted]"}
else
{name, value}
end
case request.headers do
headers when is_map(headers) ->
for {name, values} <- request.headers, into: %{} do
if name in ["authorization", "Authorization"] do
[_] = values
{name, ["[redacted]"]}
else
{name, values}
end
end

# TODO: headers are always a map on Req v1.0
#
end

options =
Expand Down
36 changes: 24 additions & 12 deletions lib/req/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ defmodule Req.Response do

@type t() :: %__MODULE__{
status: non_neg_integer(),
headers: [{binary(), binary()}],
headers: %{binary() => [binary()]},
body: binary() | term(),
private: map()
}

defstruct status: 200,
headers: [],
headers: %{},
body: "",
private: %{}

Expand All @@ -35,11 +35,11 @@ defmodule Req.Response do
## Example
iex> Req.Response.new(status: 200, body: "body")
%Req.Response{status: 200, headers: [], body: "body"}
%Req.Response{status: 200, headers: %{}, body: "body"}
iex> finch_response = %Finch.Response{status: 200}
iex> Req.Response.new(finch_response)
%Req.Response{status: 200, headers: [], body: ""}
%Req.Response{status: 200, headers: %{}, body: ""}
"""
@spec new(options :: keyword() | map() | struct()) :: t()
Expand All @@ -48,7 +48,18 @@ defmodule Req.Response do
def new(options) when is_list(options), do: new(Map.new(options))

def new(options) do
options = Map.take(options, [:status, :headers, :body])
options =
Map.take(options, [:status, :headers, :body])
|> Map.update(:headers, %{}, fn
map when is_map(map) ->
map

list when is_list(list) ->
Enum.reduce(list, %{}, fn {name, value}, acc ->
Map.update(acc, name, [value], &(&1 ++ [value]))
end)
end)

struct!(__MODULE__, options)
end

Expand All @@ -60,15 +71,15 @@ defmodule Req.Response do
iex> Req.Response.json(%{hello: 42})
%Req.Response{
status: 200,
headers: [{"content-type", "application/json"}],
headers: %{"content-type" => ["application/json"]},
body: ~s|{"hello":42}|
}
iex> resp = Req.Response.new()
iex> Req.Response.json(resp, %{hello: 42})
%Req.Response{
status: 200,
headers: [{"content-type", "application/json"}],
headers: %{"content-type" => ["application/json"]},
body: ~s|{"hello":42}|
}
Expand All @@ -79,7 +90,7 @@ defmodule Req.Response do
iex> |> Req.Response.json(%{hello: 42})
%Req.Response{
status: 200,
headers: [{"content-type", "application/vnd.api+json; charset=utf-8"}],
headers: %{"content-type" => ["application/vnd.api+json; charset=utf-8"]},
body: ~s|{"hello":42}|
}
"""
Expand Down Expand Up @@ -123,7 +134,7 @@ defmodule Req.Response do
"""
@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
Map.get(response.headers, key, [])
end

@doc """
Expand All @@ -136,9 +147,10 @@ defmodule Req.Response do
[{"content-type", "application/json"}]
"""
@spec put_header(t(), binary(), binary()) :: t()
@spec put_header(t(), binary(), 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})}
when is_binary(key) and
(is_binary(value) or is_list(value)) do
put_in(response.headers[key], List.wrap(value))
end
end
Loading

0 comments on commit 6ce83c9

Please sign in to comment.