Skip to content

Commit

Permalink
Add HTTPoison.Request.to_curl/1 to get an equivalent curl command (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
dariodf authored May 17, 2022
1 parent a3c7c02 commit 2fca7b8
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 16 deletions.
102 changes: 102 additions & 0 deletions lib/httpoison.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,108 @@ defmodule HTTPoison.Request do
params: params,
options: options
}

@doc """
Returns an equivalent `curl` command for the given request.
## Examples
iex> request = %HTTPoison.Request{url: "https://api.github.com", method: :get, headers: [{"Content-Type", "application/json"}]}
iex> HTTPoison.Request.to_curl(request)
"curl -X GET -H 'Content-Type: application/json' https://api.github.com ;"
iex> request = HTTPoison.get!("https://api.github.com", [{"Content-Type", "application/json"}]).request
iex> HTTPoison.Request.to_curl(request)
"curl -X GET -H 'Content-Type: application/json' https://api.github.com ;"
"""
@spec to_curl(t()) :: {:ok, binary()} | {:error, atom()}
def to_curl(request = %__MODULE__{}) do
options =
Enum.reduce(request.options, [], fn
{:timeout, timeout}, acc ->
["--connect-timeout #{Float.round(timeout / 1000, 3)}" | acc]

{:recv_timeout, timeout}, acc ->
["--max-time #{Float.round(timeout / 1000, 3)}" | acc]

{:proxy, {:socks5, host, port}}, acc ->
proxy_auth =
if request.options[:socks5_user] do
user = request.options[:socks5_user]
pass = request.options[:socks5_pass]
" --proxy-basic --proxy-user #{user}:#{pass}"
end

["--socks5 #{host}:#{port}#{proxy_auth}" | acc]

{:proxy, {host, port}}, acc ->
["--proxy #{host}:#{port}" | acc]

{:proxy_auth, {user, pass}}, acc ->
["--proxy-user #{user}:#{pass}" | acc]

{:ssl, ssl_opts}, acc ->
ssl_opts =
Enum.reduce(ssl_opts, [], fn
{:keyfile, keyfile}, acc -> ["--key #{keyfile}" | acc]
{:certfile, certfile}, acc -> ["--cert #{certfile}" | acc]
{:cacertfile, cacertfile}, acc -> ["--cacert #{cacertfile}" | acc]
end)
|> Enum.join(" ")

[ssl_opts | acc]

{:follow_redirect, true}, acc ->
max_redirs = Keyword.get(request.options, :max_redirect, 5)
["-L --max-redirs #{max_redirs}" | acc]

{:hackney, _}, _ ->
throw({:error, :hackney_opts_not_supported})

_, acc ->
acc
end)
|> Enum.join(" ")

{scheme_opts, url} =
case URI.parse(request.url) do
%URI{scheme: "http+unix"} = uri ->
uri = %URI{uri | scheme: "http", host: nil, authority: nil}
{"--unix-socket #{uri.host}", URI.to_string(uri)}

_ ->
{"", request.url}
end

method = "-X " <> (request.method |> to_string() |> String.upcase())
headers = request.headers |> Enum.map(fn {k, v} -> "-H '#{k}: #{v}'" end) |> Enum.join(" ")

body =
case request.body do
"" -> ""
{:file, filename} -> "-d @#{filename}"
{:form, form} -> form |> Enum.map(fn {k, v} -> "-F '#{k}=#{v}'" end) |> Enum.join(" ")
{:stream, stream} -> "-d '#{Enum.join(stream, "")}'"
{:multipart, _} -> throw({:error, :multipart_not_supported})
body when is_binary(body) -> "-d '#{body}'"
_ -> ""
end

{:ok,
[
"curl",
options,
scheme_opts,
method,
headers,
body,
url
]
|> Enum.map(&String.trim/1)
|> Enum.filter(&(&1 != ""))
|> Enum.join(" ")}
catch
e -> e
end
end

defmodule HTTPoison.Response do
Expand Down
107 changes: 91 additions & 16 deletions test/httpoison_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule HTTPoisonTest do
use ExUnit.Case, async: true
import PathHelpers
alias Jason
alias HTTPoison.Request

test "get" do
assert_response(HTTPoison.get("localhost:8080/deny"), fn response ->
Expand All @@ -17,6 +18,9 @@ defmodule HTTPoisonTest do
assert args["foo"] == "bar"
assert args["baz"] == "bong"
assert args |> Map.keys() |> length == 2

assert Request.to_curl(response.request) ==
{:ok, "curl -X GET http://localhost:8080/get?baz=bong&foo=bar"}
end)
end

Expand All @@ -34,23 +38,33 @@ defmodule HTTPoisonTest do
assert args["baz"] == "bong"
assert args["bar"] == "zing"
assert args |> Map.keys() |> length == 3

assert Request.to_curl(response.request) ==
{:ok,
"curl -X GET http://localhost:8080/get?bar=zing&foo=first&foo=second&baz=bong"}
end)
end

test "head" do
assert_response(HTTPoison.head("localhost:8080/get"), fn response ->
assert response.body == ""
assert Request.to_curl(response.request) == {:ok, "curl -X HEAD http://localhost:8080/get"}
end)
end

test "post charlist body" do
assert_response(HTTPoison.post("localhost:8080/post", 'test'))
assert_response(HTTPoison.post("localhost:8080/post", 'test'), fn response ->
assert Request.to_curl(response.request) == {:ok, "curl -X POST http://localhost:8080/post"}
end)
end

test "post binary body" do
{:ok, file} = File.read(fixture_path("image.png"))

assert_response(HTTPoison.post("localhost:8080/post", file))
assert_response(HTTPoison.post("localhost:8080/post", file), fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -X POST -d '#{file}' http://localhost:8080/post"}
end)
end

test "post form data" do
Expand All @@ -60,30 +74,49 @@ defmodule HTTPoisonTest do
}),
fn response ->
Regex.match?(~r/"key".*"value"/, response.body)

assert Request.to_curl(response.request) ==
{:ok,
"curl -X POST -H 'Content-type: application/x-www-form-urlencoded' -F 'key=value' http://localhost:8080/post"}
end
)
end

test "put" do
assert_response(HTTPoison.put("localhost:8080/put", "test"))
assert_response(HTTPoison.put("localhost:8080/put", "test"), fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -X PUT -d 'test' http://localhost:8080/put"}
end)
end

test "put without body" do
assert_response(HTTPoison.put("localhost:8080/put"))
assert_response(HTTPoison.put("localhost:8080/put"), fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -X PUT http://localhost:8080/put"}
end)
end

test "patch" do
assert_response(HTTPoison.patch("localhost:8080/patch", "test"))
assert_response(HTTPoison.patch("localhost:8080/patch", "test"), fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -X PATCH -d 'test' http://localhost:8080/patch"}
end)
end

test "delete" do
assert_response(HTTPoison.delete("localhost:8080/delete"))
assert_response(HTTPoison.delete("localhost:8080/delete"), fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -X DELETE http://localhost:8080/delete"}
end)
end

test "options" do
assert_response(HTTPoison.options("localhost:8080/get"), fn response ->
assert get_header(response.headers, "content-length") == "0"
assert is_binary(get_header(response.headers, "allow"))

assert Request.to_curl(response.request) ==
{:ok, "curl -X OPTIONS http://localhost:8080/get"}
end)
end

Expand All @@ -93,13 +126,22 @@ defmodule HTTPoisonTest do
"http://localhost:8080/redirect-to?url=http%3A%2F%2Flocalhost:8080%2Fget",
[],
follow_redirect: true
)
),
fn response ->
assert Request.to_curl(response.request) ==
{:ok,
"curl -L --max-redirs 5 -X GET http://localhost:8080/redirect-to?url=http%3A%2F%2Flocalhost:8080%2Fget"}
end
)
end

test "option follow redirect relative url" do
assert_response(
HTTPoison.get("http://localhost:8080/relative-redirect/1", [], follow_redirect: true)
HTTPoison.get("http://localhost:8080/relative-redirect/1", [], follow_redirect: true),
fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -L --max-redirs 5 -X GET http://localhost:8080/relative-redirect/1"}
end
)
end

Expand All @@ -112,7 +154,10 @@ defmodule HTTPoisonTest do
end

test "explicit http scheme" do
assert_response(HTTPoison.head("http://localhost:8080/get"))
assert_response(HTTPoison.head("http://localhost:8080/get"), fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -X HEAD http://localhost:8080/get"}
end)
end

test "https scheme" do
Expand All @@ -126,7 +171,12 @@ defmodule HTTPoisonTest do
"https://localhost:8433/get",
[],
ssl: [cacertfile: cacert_file, keyfile: key_file, certfile: cert_file]
)
),
fn response ->
assert Request.to_curl(response.request) ==
{:ok,
"curl --cert #{cert_file} --key #{key_file} --cacert #{cacert_file} -X GET https://localhost:8433/get"}
end
)
end

Expand All @@ -135,7 +185,14 @@ defmodule HTTPoisonTest do
case {HTTParrot.unix_socket_supported?(), Application.fetch_env(:httparrot, :socket_path)} do
{true, {:ok, path}} ->
path = URI.encode_www_form(path)
assert_response(HTTPoison.get("http+unix://#{path}/get"))

assert_response(
HTTPoison.get("http+unix://#{path}/get"),
fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl --unix-socket #{path} -X GET http:/get"}
end
)

_ ->
:ok
Expand All @@ -144,18 +201,29 @@ defmodule HTTPoisonTest do
end

test "char list URL" do
assert_response(HTTPoison.head('localhost:8080/get'))
assert_response(HTTPoison.head('localhost:8080/get'), fn response ->
assert Request.to_curl(response.request) ==
{:ok, "curl -X HEAD http://localhost:8080/get"}
end)
end

test "request headers as a map" do
map_header = %{"X-Header" => "X-Value"}
assert HTTPoison.get!("localhost:8080/get", map_header).body =~ "X-Value"
assert response = HTTPoison.get!("localhost:8080/get", map_header)
assert response.body =~ "X-Value"

assert Request.to_curl(response.request) ==
{:ok, "curl -X GET -H 'X-Header: X-Value' http://localhost:8080/get"}
end

test "cached request" do
if_modified = %{"If-Modified-Since" => "Tue, 11 Dec 2012 10:10:24 GMT"}
response = HTTPoison.get!("localhost:8080/cache", if_modified)
assert %HTTPoison.Response{status_code: 304, body: ""} = response

assert Request.to_curl(response.request) ==
{:ok,
"curl -X GET -H 'If-Modified-Since: Tue, 11 Dec 2012 10:10:24 GMT' http://localhost:8080/cache"}
end

test "send cookies" do
Expand All @@ -168,6 +236,9 @@ defmodule HTTPoisonTest do
has_foo = Enum.member?(response.headers, {"set-cookie", "foo=1; Version=1; Path=/"})
has_bar = Enum.member?(response.headers, {"set-cookie", "bar=2; Version=1; Path=/"})
assert has_foo and has_bar

assert Request.to_curl(response.request) ==
{:ok, "curl -X GET http://localhost:8080/cookies/set?foo=1&bar=2"}
end

test "exception" do
Expand Down Expand Up @@ -239,10 +310,14 @@ defmodule HTTPoisonTest do
enumerable = Jason.encode!(expected) |> String.split("")
headers = %{"Content-type" => "application/json"}
response = HTTPoison.post("localhost:8080/post", {:stream, enumerable}, headers)
assert_response(response)
{:ok, %HTTPoison.Response{body: body}} = response

assert Jason.decode!(body)["json"] == expected
assert_response(response, fn response ->
assert Jason.decode!(response.body)["json"] == expected

assert Request.to_curl(response.request) ==
{:ok,
"curl -X POST -H 'Content-type: application/json' -d '{\"some\":\"bytes\"}' http://localhost:8080/post"}
end)
end

test "max_body_length limits body size" do
Expand Down

0 comments on commit 2fca7b8

Please sign in to comment.