From 0413e382c7976aade6711437cd53a346ba253fe3 Mon Sep 17 00:00:00 2001 From: dariodf Date: Sun, 1 May 2022 20:52:25 -0300 Subject: [PATCH 1/2] Add `HTTPoison.Request.to_curl/1` to get an equivalent curl command --- lib/httpoison.ex | 102 ++++++++++++++++++++++++++++++++++++++++ test/httpoison_test.exs | 101 ++++++++++++++++++++++++++++++++------- 2 files changed, 187 insertions(+), 16 deletions(-) diff --git a/lib/httpoison.ex b/lib/httpoison.ex index 7b7b084..138b610 100644 --- a/lib/httpoison.ex +++ b/lib/httpoison.ex @@ -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()) :: binary() + 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("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("multipart not supported") + body when is_binary(body) -> "-d '#{body}'" + _ -> "" + end + + [ + "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 diff --git a/test/httpoison_test.exs b/test/httpoison_test.exs index 706862b..a10270d 100644 --- a/test/httpoison_test.exs +++ b/test/httpoison_test.exs @@ -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 -> @@ -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) == + "curl -X GET http://localhost:8080/get?baz=bong&foo=bar ;" end) end @@ -34,23 +38,32 @@ defmodule HTTPoisonTest do assert args["baz"] == "bong" assert args["bar"] == "zing" assert args |> Map.keys() |> length == 3 + + assert Request.to_curl(response.request) == + "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) == "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) == "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) == + "curl -X POST -d '#{file}' http://localhost:8080/post ;" + end) end test "post form data" do @@ -60,30 +73,48 @@ defmodule HTTPoisonTest do }), fn response -> Regex.match?(~r/"key".*"value"/, response.body) + + assert Request.to_curl(response.request) == + "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) == + "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) == + "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) == + "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) == + "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) == + "curl -X OPTIONS http://localhost:8080/get ;" end) end @@ -93,13 +124,21 @@ 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) == + "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) == + "curl -L --max-redirs 5 -X GET http://localhost:8080/relative-redirect/1 ;" + end ) end @@ -112,7 +151,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) == + "curl -X HEAD http://localhost:8080/get ;" + end) end test "https scheme" do @@ -126,7 +168,11 @@ 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) == + "curl --cert #{cert_file} --key #{key_file} --cacert #{cacert_file} -X GET https://localhost:8433/get ;" + end ) end @@ -135,7 +181,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) == + "curl --unix-socket #{path} -X GET http:/get ;" + end + ) _ -> :ok @@ -144,18 +197,28 @@ 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) == + "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) == + "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) == + "curl -X GET -H 'If-Modified-Since: Tue, 11 Dec 2012 10:10:24 GMT' http://localhost:8080/cache ;" end test "send cookies" do @@ -168,6 +231,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) == + "curl -X GET http://localhost:8080/cookies/set?foo=1&bar=2 ;" end test "exception" do @@ -239,10 +305,13 @@ 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) == + "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 From 54064827bd48e4ce0c13a4cb146bbcc571828f3c Mon Sep 17 00:00:00 2001 From: dariodf Date: Tue, 17 May 2022 00:22:56 -0300 Subject: [PATCH 2/2] Improve API. Remove redundant semicolon. --- lib/httpoison.ex | 32 +++++++++++++-------------- test/httpoison_test.exs | 48 +++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/lib/httpoison.ex b/lib/httpoison.ex index 138b610..04228ae 100644 --- a/lib/httpoison.ex +++ b/lib/httpoison.ex @@ -68,7 +68,7 @@ defmodule HTTPoison.Request do iex> HTTPoison.Request.to_curl(request) "curl -X GET -H 'Content-Type: application/json' https://api.github.com ;" """ - @spec to_curl(t()) :: binary() + @spec to_curl(t()) :: {:ok, binary()} | {:error, atom()} def to_curl(request = %__MODULE__{}) do options = Enum.reduce(request.options, [], fn @@ -110,7 +110,7 @@ defmodule HTTPoison.Request do ["-L --max-redirs #{max_redirs}" | acc] {:hackney, _}, _ -> - throw("hackney opts not supported") + throw({:error, :hackney_opts_not_supported}) _, acc -> acc @@ -136,24 +136,24 @@ defmodule HTTPoison.Request 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("multipart not supported") + {:multipart, _} -> throw({:error, :multipart_not_supported}) body when is_binary(body) -> "-d '#{body}'" _ -> "" end - [ - "curl", - options, - scheme_opts, - method, - headers, - body, - url, - ";" - ] - |> Enum.map(&String.trim/1) - |> Enum.filter(&(&1 != "")) - |> Enum.join(" ") + {:ok, + [ + "curl", + options, + scheme_opts, + method, + headers, + body, + url + ] + |> Enum.map(&String.trim/1) + |> Enum.filter(&(&1 != "")) + |> Enum.join(" ")} catch e -> e end diff --git a/test/httpoison_test.exs b/test/httpoison_test.exs index a10270d..36565c7 100644 --- a/test/httpoison_test.exs +++ b/test/httpoison_test.exs @@ -20,7 +20,7 @@ defmodule HTTPoisonTest do assert args |> Map.keys() |> length == 2 assert Request.to_curl(response.request) == - "curl -X GET http://localhost:8080/get?baz=bong&foo=bar ;" + {:ok, "curl -X GET http://localhost:8080/get?baz=bong&foo=bar"} end) end @@ -40,20 +40,21 @@ defmodule HTTPoisonTest do assert args |> Map.keys() |> length == 3 assert Request.to_curl(response.request) == - "curl -X GET http://localhost:8080/get?bar=zing&foo=first&foo=second&baz=bong ;" + {: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) == "curl -X HEAD http://localhost:8080/get ;" + 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'), fn response -> - assert Request.to_curl(response.request) == "curl -X POST http://localhost:8080/post ;" + assert Request.to_curl(response.request) == {:ok, "curl -X POST http://localhost:8080/post"} end) end @@ -62,7 +63,7 @@ defmodule HTTPoisonTest do assert_response(HTTPoison.post("localhost:8080/post", file), fn response -> assert Request.to_curl(response.request) == - "curl -X POST -d '#{file}' http://localhost:8080/post ;" + {:ok, "curl -X POST -d '#{file}' http://localhost:8080/post"} end) end @@ -75,7 +76,8 @@ defmodule HTTPoisonTest do Regex.match?(~r/"key".*"value"/, response.body) assert Request.to_curl(response.request) == - "curl -X POST -H 'Content-type: application/x-www-form-urlencoded' -F 'key=value' http://localhost:8080/post ;" + {:ok, + "curl -X POST -H 'Content-type: application/x-www-form-urlencoded' -F 'key=value' http://localhost:8080/post"} end ) end @@ -83,28 +85,28 @@ defmodule HTTPoisonTest do test "put" do assert_response(HTTPoison.put("localhost:8080/put", "test"), fn response -> assert Request.to_curl(response.request) == - "curl -X PUT -d 'test' http://localhost:8080/put ;" + {:ok, "curl -X PUT -d 'test' http://localhost:8080/put"} end) end test "put without body" do assert_response(HTTPoison.put("localhost:8080/put"), fn response -> assert Request.to_curl(response.request) == - "curl -X PUT http://localhost:8080/put ;" + {:ok, "curl -X PUT http://localhost:8080/put"} end) end test "patch" do assert_response(HTTPoison.patch("localhost:8080/patch", "test"), fn response -> assert Request.to_curl(response.request) == - "curl -X PATCH -d 'test' http://localhost:8080/patch ;" + {:ok, "curl -X PATCH -d 'test' http://localhost:8080/patch"} end) end test "delete" do assert_response(HTTPoison.delete("localhost:8080/delete"), fn response -> assert Request.to_curl(response.request) == - "curl -X DELETE http://localhost:8080/delete ;" + {:ok, "curl -X DELETE http://localhost:8080/delete"} end) end @@ -114,7 +116,7 @@ defmodule HTTPoisonTest do assert is_binary(get_header(response.headers, "allow")) assert Request.to_curl(response.request) == - "curl -X OPTIONS http://localhost:8080/get ;" + {:ok, "curl -X OPTIONS http://localhost:8080/get"} end) end @@ -127,7 +129,8 @@ defmodule HTTPoisonTest do ), fn response -> assert Request.to_curl(response.request) == - "curl -L --max-redirs 5 -X GET http://localhost:8080/redirect-to?url=http%3A%2F%2Flocalhost:8080%2Fget ;" + {:ok, + "curl -L --max-redirs 5 -X GET http://localhost:8080/redirect-to?url=http%3A%2F%2Flocalhost:8080%2Fget"} end ) end @@ -137,7 +140,7 @@ defmodule HTTPoisonTest do HTTPoison.get("http://localhost:8080/relative-redirect/1", [], follow_redirect: true), fn response -> assert Request.to_curl(response.request) == - "curl -L --max-redirs 5 -X GET http://localhost:8080/relative-redirect/1 ;" + {:ok, "curl -L --max-redirs 5 -X GET http://localhost:8080/relative-redirect/1"} end ) end @@ -153,7 +156,7 @@ defmodule HTTPoisonTest do test "explicit http scheme" do assert_response(HTTPoison.head("http://localhost:8080/get"), fn response -> assert Request.to_curl(response.request) == - "curl -X HEAD http://localhost:8080/get ;" + {:ok, "curl -X HEAD http://localhost:8080/get"} end) end @@ -171,7 +174,8 @@ defmodule HTTPoisonTest do ), fn response -> assert Request.to_curl(response.request) == - "curl --cert #{cert_file} --key #{key_file} --cacert #{cacert_file} -X GET https://localhost:8433/get ;" + {:ok, + "curl --cert #{cert_file} --key #{key_file} --cacert #{cacert_file} -X GET https://localhost:8433/get"} end ) end @@ -186,7 +190,7 @@ defmodule HTTPoisonTest do HTTPoison.get("http+unix://#{path}/get"), fn response -> assert Request.to_curl(response.request) == - "curl --unix-socket #{path} -X GET http:/get ;" + {:ok, "curl --unix-socket #{path} -X GET http:/get"} end ) @@ -199,7 +203,7 @@ defmodule HTTPoisonTest do test "char list URL" do assert_response(HTTPoison.head('localhost:8080/get'), fn response -> assert Request.to_curl(response.request) == - "curl -X HEAD http://localhost:8080/get ;" + {:ok, "curl -X HEAD http://localhost:8080/get"} end) end @@ -209,7 +213,7 @@ defmodule HTTPoisonTest do assert response.body =~ "X-Value" assert Request.to_curl(response.request) == - "curl -X GET -H 'X-Header: X-Value' http://localhost:8080/get ;" + {:ok, "curl -X GET -H 'X-Header: X-Value' http://localhost:8080/get"} end test "cached request" do @@ -218,7 +222,8 @@ defmodule HTTPoisonTest do assert %HTTPoison.Response{status_code: 304, body: ""} = response assert Request.to_curl(response.request) == - "curl -X GET -H 'If-Modified-Since: Tue, 11 Dec 2012 10:10:24 GMT' http://localhost:8080/cache ;" + {: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 @@ -233,7 +238,7 @@ defmodule HTTPoisonTest do assert has_foo and has_bar assert Request.to_curl(response.request) == - "curl -X GET http://localhost:8080/cookies/set?foo=1&bar=2 ;" + {:ok, "curl -X GET http://localhost:8080/cookies/set?foo=1&bar=2"} end test "exception" do @@ -310,7 +315,8 @@ defmodule HTTPoisonTest do assert Jason.decode!(response.body)["json"] == expected assert Request.to_curl(response.request) == - "curl -X POST -H 'Content-type: application/json' -d '{\"some\":\"bytes\"}' http://localhost:8080/post ;" + {:ok, + "curl -X POST -H 'Content-type: application/json' -d '{\"some\":\"bytes\"}' http://localhost:8080/post"} end) end