Skip to content

Commit

Permalink
Req.Request, Req.Response: Change headers to be maps (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach authored Aug 25, 2023
1 parent b4fa02c commit 8e87950
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 206 deletions.
58 changes: 38 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@

## HEAD

* Change `request.headers` and `response.headers` to be maps.

Previously headers were lists of name/value tuples, e.g.:

[{"content-type", "text/html"}]

Now they are maps of string names and lists of values, e.g.:

%{"content-type" => ["text/html"]}

This is a major breaking change. If you cannot easily update your app
or your dependencies, do:

# config/config.exs
config :req, legacy_headers_as_lists: true

This legacy fallback will be removed on Req 1.0.

* Make `request.registered_options` internal representation private.

* Make `request.options` internal representation private.

Currently `request.options` field is a map but it may change in the future.
One possible future change is using keywords lists internally which would
allow, for example, `Req.new(params: [a: 1]) |> Req.update(params: [b: 2])`
to keep duplicate `:params` in `request.options` which would then allow to
decide the duplicate key semantics on a per-step basis. And so, for example,
[`put_params`] would _merge_ params but most steps would simply use the
first value.

To have some room for manoeuvre in the future we should stop pattern
matching on `request.options`. Calling `request.options[key]`,
`put_in(request.options[key], value)`, and
`update_in(request.options[key], fun)` _is_ allowed.
New functions [`Req.Request.get_option/3`],
[`Req.Request.fetch_option!/2`], and [`Req.Request.delete_option/1`] have been
added for additional ways to manipulate the internal representation.

* Fix typespecs for some functions

* [`decompress_body`]: Remove support for `deflate` compression
Expand Down Expand Up @@ -33,26 +71,6 @@

* [`Req.Request`]: Fix displaying redacted basic authentication

* Make `request.registered_options` internal representation private.

* Make `request.options` internal representation private.

Currently `request.options` field is a map but it may change in the future.
One possible future change is using keywords lists internally which would
allow, for example, `Req.new(params: [a: 1]) |> Req.update(params: [b: 2])`
to keep duplicate `:params` in `request.options` which would then allow to
decide the duplicate key semantics on a per-step basis. And so, for example,
[`put_params`] would _merge_ params but most steps would simply use the
first value.

To have some room for manoeuvre in the future we should stop pattern
matching on `request.options`. Calling `request.options[key]`,
`put_in(request.options[key], value)`, and
`update_in(request.options[key], fun)` _is_ allowed.
New functions [`Req.Request.get_option/3`],
[`Req.Request.fetch_option!/2`], and [`Req.Request.delete_option/1`] have been
added for additional ways to manipulate the internal representation.

## v0.3.11 (2023-07-24)

* Support `Req.get(options)`, `Req.post(options)`, etc
Expand Down
76 changes: 50 additions & 26 deletions lib/req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,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 @@ -372,9 +372,13 @@ 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
if unquote(Req.MixProject.legacy_headers_as_lists?()) do
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
else
Map.merge(old_headers, encode_headers(new_headers))
end
end)

{name, value}, acc ->
Expand Down Expand Up @@ -942,32 +946,52 @@ defmodule Req do
Application.put_env(:req, :default_options, options)
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("_", "-") |> String.downcase(:ascii)
if Req.MixProject.legacy_headers_as_lists?() do
defp encode_headers(headers) do
for {name, value} <- headers do
{encode_header_name(name), encode_header_value(value)}
end
end
else
defp encode_headers(headers) do
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

binary when is_binary(binary) ->
String.downcase(binary, :ascii)
end
defp encode_header_values([value | rest]) do
[encode_header_value(value) | encode_header_values(rest)]
end

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

%NaiveDateTime{} = datetime ->
IO.warn("setting header to %NaiveDateTime{} is deprecated, use %DateTime{} instead")
Req.Steps.format_http_datetime(datetime)
defp encode_header_name(name) when is_atom(name) do
name |> Atom.to_string() |> String.replace("_", "-") |> __ensure_header_downcase__()
end

_ ->
String.Chars.to_string(value)
end
defp encode_header_name(name) when is_binary(name) do
__ensure_header_downcase__(name)
end

{name, value}
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
Loading

0 comments on commit 8e87950

Please sign in to comment.