Skip to content
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

Req.Request, Req.Response: Change headers to be maps #224

Merged
merged 6 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading