From 5738314358a0e4e24f41dfa9b31a7fd9a6734bb5 Mon Sep 17 00:00:00 2001 From: Jim Kane Date: Thu, 13 Feb 2025 10:41:08 -0600 Subject: [PATCH] Remove httpc HTTPAdapter In order to speed up this iteration, defer implementation of an `:httpc` based adapter until a later time. --- lib/honeybadger/http_adapter.ex | 7 +- lib/honeybadger/http_adapter/httpc.ex | 209 ------------------- test/honeybadger/http_adapter/httpc_test.exs | 115 ---------- 3 files changed, 3 insertions(+), 328 deletions(-) delete mode 100644 lib/honeybadger/http_adapter/httpc.ex delete mode 100644 test/honeybadger/http_adapter/httpc_test.exs diff --git a/lib/honeybadger/http_adapter.ex b/lib/honeybadger/http_adapter.ex index a7bc4ab..5af5ddb 100644 --- a/lib/honeybadger/http_adapter.ex +++ b/lib/honeybadger/http_adapter.ex @@ -5,15 +5,15 @@ defmodule Honeybadger.HTTPAdapter do You can configure the HTTP adapter that Honeybadger uses by setting the following option: - http_adapter: Honeybadger.HTTPAdapter.Httpc + http_adapter: Honeybadger.HTTPAdapter.Req Default options can be set by passing a list of options: - http_adapter: {Honeybadger.HTTPAdapter.Httpc, [...]} + http_adapter: {Honeybadger.HTTPAdapter.Req, [...]} You can also set the client for the application: - config :honeybadger, :http_adapter, Honeybadger.HTTPAdapter.Httpc + config :honeybadger, :http_adapter, Honeybadger.HTTPAdapter.Req ## Usage defmodule MyApp.MyHTTPAdapter do @@ -85,7 +85,6 @@ defmodule Honeybadger.HTTPAdapter do @default_http_client Enum.find_value( [ {Req, Honeybadger.HTTPAdapter.Req}, - {:httpc, Honeybadger.HTTPAdapter.Httpc}, {Finch, Honeybadger.HTTPAdapter.Finch}, {:hackney, Honeybadger.HTTPAdapter.Hackney} ], diff --git a/lib/honeybadger/http_adapter/httpc.ex b/lib/honeybadger/http_adapter/httpc.ex deleted file mode 100644 index f134a3b..0000000 --- a/lib/honeybadger/http_adapter/httpc.ex +++ /dev/null @@ -1,209 +0,0 @@ -defmodule Honeybadger.HTTPAdapter.Httpc do - @moduledoc """ - HTTP adapter module for making http requests with `:httpc`. - - SSL support will be enabled automatically if the `:certifi` and - `:ssl_verify_fun` libraries exist in your project. You can also - override the `:httpc` options by updating the configuration: - - http_adapter: {Honeybadger.HTTPAdapter.Httpc, [...]} - - For releases please make sure you have included `:inets` in your - application: - - extra_applications: [:inets] - - See `Honeybadger.HTTPAdapter` for more. - """ - - alias Honeybadger.{HTTPAdapter, HTTPAdapter.HTTPResponse} - - @behaviour HTTPAdapter - - @impl HTTPAdapter - def request(method, url, body, headers, httpc_opts \\ nil) do - raise_on_missing_httpc!() - - headers = headers ++ [HTTPAdapter.user_agent_header()] - request = httpc_request(url, body, headers) - opts = parse_httpc_ssl_opts(httpc_opts, url) - - method - |> :httpc.request(request, opts, []) - |> format_response() - end - - defp raise_on_missing_httpc! do - Code.ensure_loaded?(:httpc) || - raise """ - #{inspect(__MODULE__)} requires `:httpc` to be included in your - application. - - Please add `:inets` to `:extra_applications`: - - def application do - [ - # ... - extra_applications: [ - # ... - :inets - ] - ] - """ - end - - defp httpc_request(url, body, headers) do - url = to_charlist(url) - headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end) - - do_httpc_request(url, body, headers) - end - - defp do_httpc_request(url, nil, headers) do - {url, headers} - end - - defp do_httpc_request(url, body, headers) do - {content_type, headers} = split_content_type_headers(headers) - body = to_charlist(body) - headers = set_content_length_header(headers, body) - - {url, headers, content_type, body} - end - - defp split_content_type_headers(headers) do - case List.keytake(headers, ~c"content-type", 0) do - nil -> {~c"text/plain", headers} - {{_, ct}, headers} -> {ct, headers} - end - end - - defp set_content_length_header(headers, body) do - case List.keyfind(headers, ~c"content-length", 0) do - nil -> - length = body |> IO.iodata_length() |> Integer.to_string() |> to_charlist() - [{~c"content-length", length} | headers] - - _ -> - headers - end - end - - defp format_response({:ok, {{_, status, _}, headers, body}}) do - headers = - Enum.map(headers, fn {key, value} -> - {String.downcase(to_string(key)), to_string(value)} - end) - - body = IO.iodata_to_binary(body) - - {:ok, %HTTPResponse{status: status, headers: headers, body: body}} - end - - defp format_response({:error, error}), do: {:error, error} - - defp parse_httpc_ssl_opts(nil, url), do: parse_httpc_ssl_opts([], url) - - defp parse_httpc_ssl_opts(opts, url) do - uri = URI.parse(url) - - case uri.scheme do - "https" -> - ssl_opts = - opts - |> Keyword.get(:ssl, []) - |> verify_fun_ssl_opts(uri) - |> cacerts_ssl_opts() - - Keyword.put(opts, :ssl, ssl_opts) - - "http" -> - opts - end - end - - defp verify_fun_ssl_opts(ssl_opts, uri) do - case Keyword.has_key?(ssl_opts, :verify_fun) do - true -> - ssl_opts - - false -> - raise_on_missing_ssl_verify_fun!() - - # This handles certificates for wildcard domain with SAN extension for - # OTP >= 22 - hostname_match_check = - try do - [ - customize_hostname_check: [ - match_fun: :public_key.pkix_verify_hostname_match_fun(:https) - ] - ] - rescue - _e in UndefinedFunctionError -> [] - end - - Keyword.merge( - [ - verify: :verify_peer, - depth: 99, - verify_fun: - {&:ssl_verify_hostname.verify_fun/3, check_hostname: to_charlist(uri.host)} - ] ++ hostname_match_check, - ssl_opts - ) - end - end - - defp raise_on_missing_ssl_verify_fun! do - Code.ensure_loaded?(:ssl_verify_hostname) || - raise """ - This request's SSL certificate cannot be verified. - - Please add `:ssl_verify_fun` to your project dependencies: - - {:ssl_verify_fun, "~> 1.1"} - - Or specify the ssl options in the `:http_adapter` config option: - - config = - [ - client_id: "REPLACE_WITH_CLIENT_ID", - client_secret: "REPLACE_WITH_CLIENT_SECRET", - http_adapter: {#{__MODULE__}, ssl: [verify_peer: :verify_peer, verify_fun: ...]} - ] - """ - end - - defp cacerts_ssl_opts(ssl_opts) do - case Keyword.has_key?(ssl_opts, :cacerts) || Keyword.has_key?(ssl_opts, :cacertfile) do - true -> - ssl_opts - - false -> - raise_on_missing_certifi!() - - ssl_opts ++ [cacerts: :certifi.cacerts()] - end - end - - defp raise_on_missing_certifi! do - Code.ensure_loaded?(:certifi) || - raise """ - This request requires a CA trust store. - - Please add `:certifi` to your project dependencies: - - {:certifi, "~> 2.4"} - - Or specify the ssl options in the `:http_adapter` config option: - - config = - [ - client_id: "REPLACE_WITH_CLIENT_ID", - client_secret: "REPLACE_WITH_CLIENT_SECRET", - http_adapter: {#{__MODULE__}, ssl: [cacerts: ...]} - ] - """ - end -end diff --git a/test/honeybadger/http_adapter/httpc_test.exs b/test/honeybadger/http_adapter/httpc_test.exs deleted file mode 100644 index 1a91d74..0000000 --- a/test/honeybadger/http_adapter/httpc_test.exs +++ /dev/null @@ -1,115 +0,0 @@ -defmodule Honeybadger.HTTPAdapter.HttpcTest do - use Honeybadger.Case - doctest Honeybadger.HTTPAdapter.Httpc - - alias Honeybadger.HTTPAdapter.{Httpc, HTTPResponse} - - describe "request/4" do - test "handles SSL" do - TestServer.start(scheme: :https) - TestServer.add("/", via: :get) - - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = - Httpc.request(:get, TestServer.url(), nil, [], - ssl: [cacerts: TestServer.x509_suite().cacerts] - ) - - File.mkdir_p!("tmp") - - File.write!( - "tmp/cacerts.pem", - :public_key.pem_encode( - Enum.map(TestServer.x509_suite().cacerts, &{:Certificate, &1, :not_encrypted}) - ) - ) - - TestServer.add("/", via: :get) - - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = - Httpc.request(:get, TestServer.url(), nil, [], - ssl: [cacertfile: ~c"tmp/cacerts.pem"] - ) - end - - test "handles SSL with bad certificate" do - TestServer.start(scheme: :https) - - bad_host_url = TestServer.url(host: "bad-host.localhost") - httpc_opts = [ssl: [cacerts: TestServer.x509_suite().cacerts]] - - assert {:error, {:failed_connect, error}} = - Httpc.request(:get, bad_host_url, nil, [], httpc_opts) - - assert {:tls_alert, {:handshake_failure, _error}} = inet_error(error) - end - - test "handles SSL with bad certificate and no verification" do - TestServer.start(scheme: :https) - TestServer.add("/", via: :get) - - bad_host_url = TestServer.url(host: "bad-host.localhost") - - httpc_opts = [ - ssl: [ - cacerts: TestServer.x509_suite().cacerts, - verify: :verify_none, - verify_fun: {fn _cert, _event, state -> {:valid, state} end, nil} - ] - ] - - assert {:ok, %HTTPResponse{status: 200}} = - Httpc.request(:get, bad_host_url, nil, [], httpc_opts) - end - - test "handles unreachable host" do - TestServer.start() - url = TestServer.url() - TestServer.stop() - - assert {:error, {:failed_connect, error}} = Httpc.request(:get, url, nil, []) - assert inet_error(error) == :econnrefused - end - - test "handles query in URL" do - TestServer.add("/get", - via: :get, - to: fn conn -> - assert conn.query_string == "a=1" - - Plug.Conn.send_resp(conn, 200, "") - end - ) - - assert {:ok, %HTTPResponse{status: 200}} = - Httpc.request(:get, TestServer.url("/get?a=1"), nil, []) - end - - test "handles POST" do - TestServer.add("/post", - via: :post, - to: fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn, []) - params = URI.decode_query(body) - - assert params["a"] == "1" - assert params["b"] == "2" - - assert Plug.Conn.get_req_header(conn, "content-type") == [ - "application/x-www-form-urlencoded" - ] - - assert Plug.Conn.get_req_header(conn, "content-length") == ["7"] - - Plug.Conn.send_resp(conn, 200, "") - end - ) - - assert {:ok, %HTTPResponse{status: 200}} = - Httpc.request(:post, TestServer.url("/post"), "a=1&b=2", [ - {"content-type", "application/x-www-form-urlencoded"} - ]) - end - end - - defp inet_error([_, {:inet, [:inet], error}]), do: error -end