Skip to content

Commit

Permalink
Allow to specify encoding strategy for query params
Browse files Browse the repository at this point in the history
  • Loading branch information
RudolfMan authored and yordis committed Oct 24, 2024
1 parent ca26020 commit 0c3a7b1
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 14 deletions.
31 changes: 25 additions & 6 deletions lib/tesla.ex
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ defmodule Tesla do
Useful when you need to create an URL with dynamic query params from a Keyword list
Allows to specify the `encoding` strategy to be one either `:www_form` or `:rfc3986`
Read more about options in [`URI.encode_query/2`](https://hexdocs.pm/elixir/1.14.3/URI.html#encode_query/2)
Defaults to `:www_form`, ignored when compiled with elixir version older than 1.12.
## Examples
iex> Tesla.build_url("http://api.example.com", [user: 3, page: 2])
Expand All @@ -306,19 +310,34 @@ defmodule Tesla do
iex> Tesla.build_url(url, [page: 2, status: true])
"http://api.example.com?user=3&page=2&status=true"
# default encoding `:www_form`
iex> Tesla.build_url("http://api.example.com", [user_name: "John Smith"])
"http://api.example.com?user_name=John+Smith"
# specified encoding strategy :rfc3986
iex> Tesla.build_url("http://api.example.com", [user_name: "John Smith"], :rfc3986)
"http://api.example.com?user_name=John%20Smith"
"""
@spec build_url(Tesla.Env.url(), Tesla.Env.query()) :: binary
def build_url(url, []), do: url
@spec build_url(Tesla.Env.url(), Tesla.Env.query(), :rfc3986 | :www_form) :: binary
def build_url(url, query, encoding \\ :www_form)

def build_url(url, [], _encoding), do: url

def build_url(url, query) do
def build_url(url, query, encoding) do
join = if String.contains?(url, "?"), do: "&", else: "?"
url <> join <> encode_query(query)
url <> join <> encode_query(query, encoding)
end

def encode_query(query) do
def encode_query(query, encoding \\ :www_form) do
query
|> Enum.flat_map(&encode_pair/1)
|> URI.encode_query()
|> uri_encode_query(encoding)
end

if Version.match?(System.version(), "~> 1.12") do
defp uri_encode_query(enum, encoding), do: URI.encode_query(enum, encoding)
else
defp uri_encode_query(enum, _encoding), do: URI.encode_query(enum)
end

@doc false
Expand Down
3 changes: 2 additions & 1 deletion lib/tesla/adapter/finch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ if Code.ensure_loaded?(Finch) do

@impl Tesla.Adapter
def call(%Tesla.Env{} = env, opts) do
query_encoding = Keyword.get(env.opts, :query_encoding, :www_form)
opts = Tesla.Adapter.opts(@defaults, env, opts)

name = Keyword.fetch!(opts, :name)
url = Tesla.build_url(env.url, env.query)
url = Tesla.build_url(env.url, env.query, query_encoding)
req_opts = Keyword.take(opts, [:pool_timeout, :receive_timeout])
req = build(env.method, url, env.headers, env.body)

Expand Down
4 changes: 3 additions & 1 deletion lib/tesla/adapter/gun.ex
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,11 @@ if Code.ensure_loaded?(:gun) do
end

defp request(env, opts) do
query_encoding = Keyword.get(env.opts, :query_encoding, :www_form)

request(
Tesla.Adapter.Shared.format_method(env.method),
Tesla.build_url(env.url, env.query),
Tesla.build_url(env.url, env.query, query_encoding),
format_headers(env.headers),
env.body || "",
Tesla.Adapter.opts(
Expand Down
4 changes: 3 additions & 1 deletion lib/tesla/adapter/hackney.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ if Code.ensure_loaded?(:hackney) do
defp format_body(data) when is_binary(data) or is_reference(data), do: data

defp request(env, opts) do
query_encoding = Keyword.get(env.opts, :query_encoding, :www_form)

request(
env.method,
Tesla.build_url(env.url, env.query),
Tesla.build_url(env.url, env.query, query_encoding),
env.headers,
env.body,
Tesla.Adapter.opts(env, opts)
Expand Down
3 changes: 2 additions & 1 deletion lib/tesla/adapter/httpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ defmodule Tesla.Adapter.Httpc do

defp request(env, opts) do
content_type = to_charlist(Tesla.get_header(env, "content-type") || "")
query_encoding = Keyword.get(env.opts, :query_encoding, :www_form)

handle(
request(
env.method,
Tesla.build_url(env.url, env.query) |> to_charlist,
Tesla.build_url(env.url, env.query, query_encoding) |> to_charlist(),
Enum.map(env.headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end),
content_type,
env.body,
Expand Down
3 changes: 2 additions & 1 deletion lib/tesla/adapter/ibrowse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ if Code.ensure_loaded?(:ibrowse) do
defp format_body(data) when is_binary(data), do: data

defp request(env, opts) do
query_encoding = Keyword.get(env.opts, :query_encoding, :www_form)
body = env.body || []

handle(
request(
Tesla.build_url(env.url, env.query) |> to_charlist,
Tesla.build_url(env.url, env.query, query_encoding) |> to_charlist(),
env.headers,
env.method,
body,
Expand Down
4 changes: 3 additions & 1 deletion lib/tesla/adapter/mint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,11 @@ if Code.ensure_loaded?(Mint.HTTP) do
defdelegate close(conn), to: HTTP

defp request(env, opts) do
query_encoding = Keyword.get(env.opts, :query_encoding, :www_form)

request(
format_method(env.method),
Tesla.build_url(env.url, env.query),
Tesla.build_url(env.url, env.query, query_encoding),
env.headers,
env.body,
Enum.into(opts, %{})
Expand Down
7 changes: 6 additions & 1 deletion lib/tesla/middleware/logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ defmodule Tesla.Middleware.Logger.Formatter do
Enum.map(format, &output(&1, request, response, time))
end

defp output(:query, env, _, _), do: env.query |> Tesla.encode_query()
defp output(:query, env, _, _) do
encoding = Keyword.get(env.opts, :query_encoding, :www_form)

Tesla.encode_query(env.query, encoding)
end

defp output(:method, env, _, _), do: env.method |> to_string() |> String.upcase()
defp output(:url, env, _, _), do: env.url
defp output(:status, _, {:ok, env}, _), do: to_string(env.status)
Expand Down
29 changes: 29 additions & 0 deletions test/support/adapter_case/basic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ defmodule Tesla.AdapterCase.Basic do
assert args["user[age]"] == "20"
end

test "encoding query params with www_form by default" do
request = %Env{
method: :get,
url: "#{@http}/get",
query: [user_name: "John Smith"]
}

assert {:ok, %Env{} = response} = call(request)
assert {:ok, %Env{} = response} = Tesla.Middleware.JSON.decode(response, [])

assert response.body["url"] == "#{@http}/get?user_name=John+Smith"
end

if Version.match?(System.version(), "~> 1.12") do
test "encoding query params with rfc3986 optionally" do
request = %Env{
method: :get,
url: "#{@http}/get",
query: [user_name: "John Smith"],
opts: [query_encoding: :rfc3986]
}

assert {:ok, %Env{} = response} = call(request)
assert {:ok, %Env{} = response} = Tesla.Middleware.JSON.decode(response, [])

assert response.body["url"] == "#{@http}/get?user_name=John%20Smith"
end
end

test "autoredirects disabled by default" do
request = %Env{
method: :get,
Expand Down
18 changes: 17 additions & 1 deletion test/tesla/middleware/logger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Tesla.Middleware.LoggerTest do
defmodule Client do
use Tesla

plug Tesla.Middleware.Logger
plug Tesla.Middleware.Logger, format: "$query $url -> $status"

adapter fn env ->
env = Tesla.put_header(env, "content-type", "text/plain")
Expand Down Expand Up @@ -61,6 +61,22 @@ defmodule Tesla.Middleware.LoggerTest do
log = capture_log(fn -> Client.get("/ok") end)
assert log =~ "/ok -> 200"
end

test "default encoding strategy www_form" do
log = capture_log(fn -> Client.get("/ok", query: [test: "foo bar"]) end)
assert log =~ "test=foo+bar"
end

if Version.match?(System.version(), "~> 1.12") do
test "encodes with specified strategy" do
log =
capture_log(fn ->
Client.get("/ok", query: %{"test" => "foo bar"}, opts: [query_encoding: :rfc3986])
end)

assert log =~ "test=foo%20bar"
end
end
end

describe "Debug mode" do
Expand Down
24 changes: 24 additions & 0 deletions test/tesla_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,28 @@ defmodule TeslaTest do
end
end
end

describe "build_url/3" do
setup do
{:ok, url: "http://api.example.com"}
end

test "encoding www_form", %{url: url} do
query_params = [name: "foo bar", page: 2]
assert build_url(url, query_params, :www_form) === url <> "?name=foo+bar&page=2"
assert build_url(url, query_params, :www_form) === build_url(url, query_params)
end

if Version.match?(System.version(), "~> 1.12") do
test "encoding rfc3986", %{url: url} do
query_params = [name: "foo bar", page: 2]
assert build_url(url, query_params, :rfc3986) === url <> "?name=foo%20bar&page=2"
end
end

test "default encoding www_form", %{url: url} do
query_params = [name: "foo bar", page: 2]
assert build_url(url, query_params, :www_form) === build_url(url, query_params)
end
end
end

0 comments on commit 0c3a7b1

Please sign in to comment.