From 74fb44c86c01a1ba731d065e7696569dc44e5e4c Mon Sep 17 00:00:00 2001 From: kinjalh Date: Wed, 29 Jul 2026 10:23:19 -0400 Subject: [PATCH 1/2] httpoison to req --- lib/console/ai/graph/provider/elastic.ex | 8 +- lib/console/ai/provider/ollama.ex | 8 +- lib/console/ai/tools/workbench/http.ex | 15 +++- .../integration/azure_devops/client.ex | 26 +++---- .../workbench/integration/bitbucket/client.ex | 14 ++-- .../bitbucket_datacenter/client.ex | 22 +++--- .../workbench/integration/github/client.ex | 30 ++++---- .../workbench/integration/github/response.ex | 8 +- .../workbench/integration/gitlab/client.ex | 20 ++--- .../ai/tools/workbench/integration/http.ex | 2 +- lib/console/ai/vector/elastic.ex | 16 ++-- lib/console/clients/hydra/client.ex | 30 ++++---- lib/console/clients/loki/client.ex | 4 +- lib/console/clients/prometheus/client.ex | 16 ++-- lib/console/deployments/clusters.ex | 4 +- .../compatibilities/cloud_addons.ex | 6 +- .../deployments/compatibilities/table.ex | 2 +- lib/console/deployments/deprecations/table.ex | 2 +- lib/console/deployments/helm/repository.ex | 2 +- .../deployments/kube_versions/table.ex | 2 +- .../deployments/metrics/provider/datadog.ex | 6 +- lib/console/deployments/notifications.ex | 15 ++-- .../deployments/pr/governance/impl/webhook.ex | 9 ++- lib/console/deployments/pr/impl/azure.ex | 10 +-- lib/console/deployments/pr/impl/bitbucket.ex | 20 ++--- .../pr/impl/bitbucket_datacenter.ex | 8 +- lib/console/deployments/pr/impl/github.ex | 2 +- lib/console/deployments/pr/impl/gitlab.ex | 12 +-- lib/console/logs/provider/elastic.ex | 8 +- lib/console/logs/provider/victoria.ex | 14 ++-- lib/console/logs/stream/exec.ex | 44 ++++++----- lib/console/mesh/prometheus.ex | 4 +- lib/console/plural/client.ex | 10 ++- lib/console/utils/http.ex | 58 ++++++++++++++ lib/console_web/controllers/ai_controller.ex | 75 +++++++++++-------- lib/mix/tasks/db.certs.ex | 8 +- lib/mix/tasks/elasticsearch/up.ex | 4 +- lib/mix/tasks/prom.mocks.ex | 8 +- .../integration/github/response_test.exs | 54 +++++++------ .../integration/gitlab/client_test.exs | 4 +- .../tools/workbench/integration/http_test.exs | 8 +- test/console/deployments/cron_test.exs | 4 +- .../deployments/notifications_test.exs | 2 +- .../deployments/pr/impl/gitlab_test.exs | 44 +++++------ .../deployments/pubsub/governance_test.exs | 12 +-- .../deployments/pubsub/notification_test.exs | 68 ++++++++--------- test/console/features_test.exs | 3 +- 47 files changed, 423 insertions(+), 328 deletions(-) create mode 100644 lib/console/utils/http.ex diff --git a/lib/console/ai/graph/provider/elastic.ex b/lib/console/ai/graph/provider/elastic.ex index 7da182c765..2a6a0ff7d3 100644 --- a/lib/console/ai/graph/provider/elastic.ex +++ b/lib/console/ai/graph/provider/elastic.ex @@ -42,7 +42,7 @@ defmodule Console.AI.Graph.Provider.Elastic do def init(%__MODULE__{conn: %Elastic{index: index} = es}) do Elastic.url(es, curr_index(index)) - |> HTTPoison.put(Jason.encode!(@index_mappings), Elastic.headers(es, @headers)) + |> Req.put(headers: Elastic.headers(es, @headers), body: Jason.encode!(@index_mappings), decode_body: false, retry: false) |> handle_response("could not initialize elasticsearch:") end @@ -56,7 +56,7 @@ defmodule Console.AI.Graph.Provider.Elastic do |> Enum.join("\n") Elastic.url(es, "/_bulk") - |> HTTPoison.post("#{bulk}\n", Elastic.headers(es, [{"Content-Type", "application/x-ndjson"}])) + |> Req.post(headers: Elastic.headers(es, [{"Content-Type", "application/x-ndjson"}]), body: "#{bulk}\n", decode_body: false, retry: false) |> handle_response("could not bulk index into elasticsearch:") end @@ -133,8 +133,8 @@ defmodule Console.AI.Graph.Provider.Elastic do defp groups(%User{group_members: [_ | _] = members}), do: [%{terms: %{group_ids: Enum.map(members, & &1.group_id)}}] defp groups(_), do: [] - defp handle_response({:ok, %HTTPoison.Response{status_code: code}}, _) when code >= 200 and code < 300, do: :ok - defp handle_response({:ok, %HTTPoison.Response{body: body}}, modifier), do: {:error, "#{modifier}: #{body}"} + defp handle_response({:ok, %Req.Response{status: code}}, _) when code >= 200 and code < 300, do: :ok + defp handle_response({:ok, %Req.Response{body: body}}, modifier), do: {:error, "#{modifier}: #{body}"} defp handle_response(_, modifier), do: {:error, "#{modifier}: elasticsearch error"} def curr_index(index) when is_binary(index) do diff --git a/lib/console/ai/provider/ollama.ex b/lib/console/ai/provider/ollama.ex index bcdd144ff3..cdb4daf8be 100644 --- a/lib/console/ai/provider/ollama.ex +++ b/lib/console/ai/provider/ollama.ex @@ -14,7 +14,7 @@ defmodule Console.AI.Ollama do @base_headers [{"content-type", "application/json"}] - @options [recv_timeout: :timer.minutes(5), timeout: :timer.minutes(5)] + @options [receive_timeout: :timer.minutes(5), connect_options: [timeout: :timer.minutes(5)], decode_body: false, retry: false] defmodule Message do @type t :: %__MODULE__{} @@ -77,13 +77,13 @@ defmodule Console.AI.Ollama do }) "#{url}/api/chat" - |> HTTPoison.post(body, auth(ollama, @base_headers), @options) + |> Req.post([headers: auth(ollama, @base_headers), body: body] ++ @options) |> handle_response(ChatResponse.spec()) end - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, type) when code in 200..299, + defp handle_response({:ok, %Req.Response{status: code, body: body}}, type) when code in 200..299, do: Poison.decode(body, as: type) - defp handle_response({:ok, %HTTPoison.Response{body: body}}, _) do + defp handle_response({:ok, %Req.Response{body: body}}, _) do Logger.error "ollama error: #{body}" {:error, "ollama error: #{body}"} end diff --git a/lib/console/ai/tools/workbench/http.ex b/lib/console/ai/tools/workbench/http.ex index 750d2a2420..9f67dcd2b1 100644 --- a/lib/console/ai/tools/workbench/http.ex +++ b/lib/console/ai/tools/workbench/http.ex @@ -39,16 +39,25 @@ defmodule Console.AI.Tools.Workbench.Http do def invoke(%WorkbenchTool{configuration: %Configuration{http: http}}, %{} = input) do with {:body, {:ok, body}} <- {:body, body(http, input)}, - {:request, {:ok, %HTTPoison.Response{body: body, status_code: code}}} <- {:request, do_request(http, body)} do + {:request, {:ok, %Req.Response{body: body, status: code}}} <- {:request, do_request(http, body)} do {:ok, "http response: #{body} (status #{code})"} else {:body, {:error, error}} -> {:error, "could not render request body: #{inspect(error)}"} - {:request, {:error, %HTTPoison.Error{reason: reason}}} -> {:error, "HTTP error: #{inspect(reason)}"} + {:request, {:error, reason}} -> {:error, "HTTP error: #{inspect(reason)}"} end end defp do_request(%HttpConfiguration{method: method, url: url} = config, body) do - HTTPoison.request(method, url, body, headers(config), [timeout: 10_000, recv_timeout: 10_000]) + Req.request( + method: method, + url: url, + body: body, + headers: headers(config), + connect_options: [timeout: 10_000], + receive_timeout: 10_000, + decode_body: false, + retry: false + ) end defp headers(%HttpConfiguration{headers: [_ | _] = headers}), do: Enum.map(headers, &{&1.name, &1.value}) diff --git a/lib/console/ai/tools/workbench/integration/azure_devops/client.ex b/lib/console/ai/tools/workbench/integration/azure_devops/client.ex index edd64946b5..7c97febf51 100644 --- a/lib/console/ai/tools/workbench/integration/azure_devops/client.ex +++ b/lib/console/ai/tools/workbench/integration/azure_devops/client.ex @@ -115,11 +115,11 @@ defmodule Console.AI.Tools.Workbench.Integration.AzureDevops.Client do def get_json(%{token: _} = client, url, query \\ %{}) when is_binary(url) do req_url = url <> Query.query_string(query) - case HTTPoison.get(req_url, basic_auth_header(client), http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.get(req_url, [headers: basic_auth_header(client)] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Azure DevOps API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -132,11 +132,11 @@ defmodule Console.AI.Tools.Workbench.Integration.AzureDevops.Client do encoded = Jason.encode!(body_map) headers = json_auth_headers(client) - case HTTPoison.post(url, encoded, headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.post(url, [headers: headers, body: encoded] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Azure DevOps API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -153,11 +153,11 @@ defmodule Console.AI.Tools.Workbench.Integration.AzureDevops.Client do {Jason.encode!(body_map), json_auth_headers(client)} end - case HTTPoison.put(url, encoded, headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.put(url, [headers: headers, body: encoded] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Azure DevOps API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -167,11 +167,11 @@ defmodule Console.AI.Tools.Workbench.Integration.AzureDevops.Client do @spec post_empty(map(), String.t()) :: {:ok, term()} | {:error, String.t()} def post_empty(%{token: _} = client, url) when is_binary(url) do - case HTTPoison.post(url, "", basic_auth_header(client), http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.post(url, [headers: basic_auth_header(client), body: ""] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Azure DevOps API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -190,5 +190,5 @@ defmodule Console.AI.Tools.Workbench.Integration.AzureDevops.Client do defp http_opts, do: - Application.get_env(:console, :httpoison_azure_devops_options, []) ++ [recv_timeout: 60_000] + Application.get_env(:console, :req_azure_devops_options, []) ++ [receive_timeout: 60_000, decode_body: false, retry: false] end diff --git a/lib/console/ai/tools/workbench/integration/bitbucket/client.ex b/lib/console/ai/tools/workbench/integration/bitbucket/client.ex index 45fcb0bb0a..605f70f10f 100644 --- a/lib/console/ai/tools/workbench/integration/bitbucket/client.ex +++ b/lib/console/ai/tools/workbench/integration/bitbucket/client.ex @@ -50,11 +50,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Bitbucket.Client do def get(%{base_url: base, token: token}, path, query \\ %{}) when is_binary(path) do url = base <> path <> Query.query_string(query) - case HTTPoison.get(url, auth_headers(token), http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.get(url, [headers: auth_headers(token)] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Bitbucket Cloud API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -68,11 +68,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Bitbucket.Client do url = base <> path headers = auth_headers(token) ++ [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body_map), headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.post(url, [headers: headers, body: Jason.encode!(body_map)] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Bitbucket Cloud API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -103,5 +103,5 @@ defmodule Console.AI.Tools.Workbench.Integration.Bitbucket.Client do defp enc(s) when is_binary(s), do: URI.encode(String.trim(s), &URI.char_unreserved?/1) defp http_opts, - do: Application.get_env(:console, :httpoison_bitbucket_options, []) ++ [recv_timeout: 60_000] + do: Application.get_env(:console, :req_bitbucket_options, []) ++ [receive_timeout: 60_000, decode_body: false, retry: false] end diff --git a/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex b/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex index 04d5ea9a40..e44e1726d9 100644 --- a/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex +++ b/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex @@ -50,11 +50,11 @@ defmodule Console.AI.Tools.Workbench.Integration.BitbucketDatacenter.Client do def get(%{api_base: base, token: token}, path, query \\ %{}) when is_binary(path) do url = base <> path <> Query.query_string(query) - case HTTPoison.get(url, auth_headers(token), http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.get(url, [headers: auth_headers(token)] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Bitbucket Data Center API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -68,11 +68,11 @@ defmodule Console.AI.Tools.Workbench.Integration.BitbucketDatacenter.Client do url = base <> path headers = auth_headers(token) ++ [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body_map), headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.post(url, [headers: headers, body: Jason.encode!(body_map)] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Bitbucket Data Center API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -84,11 +84,11 @@ defmodule Console.AI.Tools.Workbench.Integration.BitbucketDatacenter.Client do def put_empty(%{token: token}, url) when is_binary(url) do headers = auth_headers(token) ++ [{"Content-Type", "application/json"}] - case HTTPoison.put(url, "", headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.put(url, [headers: headers, body: ""] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "Bitbucket Data Center API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -154,6 +154,6 @@ defmodule Console.AI.Tools.Workbench.Integration.BitbucketDatacenter.Client do defp http_opts, do: - Application.get_env(:console, :httpoison_bitbucket_datacenter_options, []) ++ - [recv_timeout: 60_000] + Application.get_env(:console, :req_bitbucket_datacenter_options, []) ++ + [receive_timeout: 60_000, decode_body: false, retry: false] end diff --git a/lib/console/ai/tools/workbench/integration/github/client.ex b/lib/console/ai/tools/workbench/integration/github/client.ex index 30f5189382..00c81df475 100644 --- a/lib/console/ai/tools/workbench/integration/github/client.ex +++ b/lib/console/ai/tools/workbench/integration/github/client.ex @@ -13,11 +13,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.Client do def plain_get(%Tentacat.Client{} = client, path, extra_headers \\ []) when is_binary(path) do url = client.endpoint <> path - case HTTPoison.request(:get, url, "", request_headers(client, extra_headers), request_options(client)) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.request(req_opts(:get, url, "", request_headers(client, extra_headers), request_options(client))) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> {:ok, body} - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "GitHub API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -112,8 +112,8 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.Client do defp json_request(method, %Tentacat.Client{} = client, path, opts \\ []) do url = client.endpoint <> path - case HTTPoison.request(method, url, "", json_headers(client), request_options(client)) do - {:ok, %HTTPoison.Response{status_code: code, body: body} = resp} -> + case Req.request(req_opts(method, url, "", json_headers(client), request_options(client))) do + {:ok, %Req.Response{status: code, body: body} = resp} -> response(method, code, decode_json_body(body), resp, opts) {:error, reason} -> @@ -121,6 +121,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.Client do end end + defp req_opts(method, url, body, headers, options) do + [method: method, url: url, body: body, headers: headers, decode_body: false, retry: false] ++ + Console.Utils.HTTP.req_options(options) + end + defp decode_json_body(body) when body in [nil, ""], do: %{} defp decode_json_body(body) do @@ -130,21 +135,20 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.Client do end end - defp response(:get, code, body, %HTTPoison.Response{} = resp, pagination: :manual), + defp response(:get, code, body, %Req.Response{} = resp, pagination: :manual), do: {{code, body, resp}, next_url(resp), nil} - defp response(:get, code, body, %HTTPoison.Response{} = resp, _) when is_list(body), + defp response(:get, code, body, %Req.Response{} = resp, _) when is_list(body), do: {{code, body, resp}, next_url(resp), nil} - defp response(_, code, body, %HTTPoison.Response{} = resp, _), + defp response(_, code, body, %Req.Response{} = resp, _), do: {code, body, resp} - defp next_url(%HTTPoison.Response{headers: headers}) do - Enum.find_value(headers, fn - {"Link", value} -> next_url(value) - {"link", value} -> next_url(value) + defp next_url(%Req.Response{} = resp) do + case Req.Response.get_header(resp, "link") do + [value | _] -> next_url(value) _ -> nil - end) + end end defp next_url(value) when is_binary(value) do diff --git a/lib/console/ai/tools/workbench/integration/github/response.ex b/lib/console/ai/tools/workbench/integration/github/response.ex index a6247f0edc..bad5c913e6 100644 --- a/lib/console/ai/tools/workbench/integration/github/response.ex +++ b/lib/console/ai/tools/workbench/integration/github/response.ex @@ -80,11 +80,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.Response do defp query_param(_, _), do: nil - defp header(%HTTPoison.Response{headers: headers}, key) do - Enum.find_value(headers, fn - {^key, value} -> value + defp header(%Req.Response{} = resp, key) do + case Req.Response.get_header(resp, String.downcase(key)) do + [value | _] -> value _ -> nil - end) + end end defp header(_, _), do: nil diff --git a/lib/console/ai/tools/workbench/integration/gitlab/client.ex b/lib/console/ai/tools/workbench/integration/gitlab/client.ex index a4265b86d8..bb16d876e9 100644 --- a/lib/console/ai/tools/workbench/integration/gitlab/client.ex +++ b/lib/console/ai/tools/workbench/integration/gitlab/client.ex @@ -39,11 +39,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Gitlab.Client do url = base <> path <> Query.query_string(query) headers = [{"PRIVATE-TOKEN", token}] - case HTTPoison.get(url, headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.get(url, [headers: headers] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "GitLab API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -57,11 +57,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Gitlab.Client do url = base <> path <> Query.query_string(query) headers = [{"PRIVATE-TOKEN", token}] - case HTTPoison.post(url, "", headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.post(url, [headers: headers, body: ""] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "GitLab API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -76,11 +76,11 @@ defmodule Console.AI.Tools.Workbench.Integration.Gitlab.Client do headers = [{"PRIVATE-TOKEN", token}, {"Content-Type", "application/json"}] encoded = Jason.encode!(body_map) - case HTTPoison.post(url, encoded, headers, http_opts()) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code >= 200 and code < 300 -> + case Req.post(url, [headers: headers, body: encoded] ++ http_opts()) do + {:ok, %Req.Response{status: code, body: body}} when code >= 200 and code < 300 -> decode_json(body) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> + {:ok, %Req.Response{status: code, body: body}} -> {:error, "GitLab API #{code}: #{inspect(body)}"} {:error, reason} -> @@ -98,7 +98,7 @@ defmodule Console.AI.Tools.Workbench.Integration.Gitlab.Client do end defp http_opts, - do: Application.get_env(:console, :httpoison_gitlab_options, []) ++ [recv_timeout: 60_000] + do: Application.get_env(:console, :req_gitlab_options, []) ++ [receive_timeout: 60_000, decode_body: false, retry: false] @doc false def encode_project_id(project) when is_integer(project), do: Integer.to_string(project) diff --git a/lib/console/ai/tools/workbench/integration/http.ex b/lib/console/ai/tools/workbench/integration/http.ex index e12ec7321c..3a855f6d5d 100644 --- a/lib/console/ai/tools/workbench/integration/http.ex +++ b/lib/console/ai/tools/workbench/integration/http.ex @@ -2,7 +2,7 @@ defmodule Console.AI.Tools.Workbench.Integration.Http do @moduledoc false @spec error(String.t(), term()) :: {:error, String.t()} - def error(service, %HTTPoison.Error{reason: reason}), + def error(service, %Req.TransportError{reason: reason}), do: {:error, "#{service} request failed: #{format_reason(reason)}"} def error(service, reason), diff --git a/lib/console/ai/vector/elastic.ex b/lib/console/ai/vector/elastic.ex index 345bf1d845..b8b9707129 100644 --- a/lib/console/ai/vector/elastic.ex +++ b/lib/console/ai/vector/elastic.ex @@ -53,7 +53,7 @@ defmodule Console.AI.Vector.Elastic do def init(%__MODULE__{conn: %Elastic{index: index} = es}) do Elastic.url(es, index) - |> HTTPoison.put(Jason.encode!(@index_mappings), Elastic.headers(es, @headers)) + |> Req.put(headers: Elastic.headers(es, @headers), body: Jason.encode!(@index_mappings), decode_body: false, retry: false) |> handle_response("could not initialize elasticsearch:") |> case do :ok -> initialized() @@ -63,7 +63,7 @@ defmodule Console.AI.Vector.Elastic do def recreate(%__MODULE__{conn: %Elastic{index: index} = es} = store) do Elastic.url(es, index) - |> HTTPoison.delete(Elastic.headers(es, @headers)) + |> Req.delete(headers: Elastic.headers(es, @headers), decode_body: false, retry: false) |> handle_response("could not delete elasticsearch:") |> case do :ok -> init(store) @@ -76,12 +76,12 @@ defmodule Console.AI.Vector.Elastic do with {id, datatype, text} <- Content.content(data), {:ok, embeddings} <- Provider.embeddings(text) do Elastic.url(es, doc_url(es.index, id)) - |> HTTPoison.post(Jason.encode!(doc_filters(%{ + |> Req.post(headers: Elastic.headers(es, @headers), body: Jason.encode!(doc_filters(%{ passages: Enum.map(embeddings, fn {passage, vector} -> %{vector: vector, text: passage} end), datatype: datatype, "@timestamp": DateTime.utc_now(), "#{datatype}": Console.mapify(data) - }, filters, conn)), Elastic.headers(es, @headers)) + }, filters, conn)), decode_body: false, retry: false) |> handle_response("could not insert vector into elasticsearch:") end end @@ -124,7 +124,7 @@ defmodule Console.AI.Vector.Elastic do not_filters = Keyword.get(opts, :not, []) query = %{query: %{bool: add_not(%{must: filters(filters)}, not_filters)}} Elastic.url(es, "#{es.index}/_delete_by_query") - |> HTTPoison.post(Jason.encode!(query), Elastic.headers(es, @headers)) + |> Req.post(headers: Elastic.headers(es, @headers), body: Jason.encode!(query), decode_body: false, retry: false) |> handle_response("could not delete vectors from elasticsearch:") end @@ -140,7 +140,7 @@ defmodule Console.AI.Vector.Elastic do } Elastic.url(es, "#{es.index}/_delete_by_query") - |> HTTPoison.post(Jason.encode!(query), Elastic.headers(es, @headers)) + |> Req.post(headers: Elastic.headers(es, @headers), body: Jason.encode!(query), decode_body: false, retry: false) |> handle_response("could not delete vectors from elasticsearch:") end @@ -190,7 +190,7 @@ defmodule Console.AI.Vector.Elastic do end defp add_not(filters, _), do: filters - defp handle_response({:ok, %HTTPoison.Response{status_code: code}}, _) when code >= 200 and code < 300, do: :ok - defp handle_response({:ok, %HTTPoison.Response{body: body}}, modifier), do: {:error, "#{modifier}: #{body}"} + defp handle_response({:ok, %Req.Response{status: code}}, _) when code >= 200 and code < 300, do: :ok + defp handle_response({:ok, %Req.Response{body: body}}, modifier), do: {:error, "#{modifier}: #{body}"} defp handle_response(_, modifier), do: {:error, "#{modifier}: elasticsearch error"} end diff --git a/lib/console/clients/hydra/client.ex b/lib/console/clients/hydra/client.ex index 76a9bdb658..a5cdfc517d 100644 --- a/lib/console/clients/hydra/client.ex +++ b/lib/console/clients/hydra/client.ex @@ -46,33 +46,33 @@ defmodule Console.Hydra.Client do def get_configuration() do public_url("/.well-known/openid-configuration") - |> HTTPoison.get(headers()) + |> Req.get(req_opts()) |> handle_response(%Configuration{}) end def get_client(id) do admin_url("/clients/#{id}") - |> HTTPoison.get(headers()) + |> Req.get(req_opts()) |> handle_response(%Client{}) end def create_client(attrs) do admin_url("/clients") - |> HTTPoison.post(Jason.encode!(attrs), headers()) + |> Req.post(req_opts(body: Jason.encode!(attrs))) |> handle_response(%Client{}) end def update_client(client_id, attrs) do admin_url("/clients/#{client_id}") - |> HTTPoison.put(Jason.encode!(attrs), headers()) + |> Req.put(req_opts(body: Jason.encode!(attrs))) |> handle_response(%Client{}) end def delete_client(client_id) do admin_url("/clients/#{client_id}") - |> HTTPoison.delete(headers()) + |> Req.delete(req_opts()) |> case do - {:ok, %{status_code: 204}} -> :ok + {:ok, %{status: 204}} -> :ok error -> Logger.error "Failed to delete hydra client: #{inspect(error)}" {:error, :unauthorized} @@ -81,26 +81,26 @@ defmodule Console.Hydra.Client do def get_login(challenge) do admin_url("/oauth2/auth/requests/login?login_challenge=#{challenge}") - |> HTTPoison.get(headers()) + |> Req.get(req_opts()) |> handle_response(%LoginRequest{client: %Client{}}) end def accept_login(challenge, user) do body = Jason.encode!(%{subject: user.id, remember: false}) admin_url("/oauth2/auth/requests/login/accept?login_challenge=#{challenge}") - |> HTTPoison.put(body, headers()) + |> Req.put(req_opts(body: body)) |> handle_response(%Response{}) end def reject_login(challenge) do admin_url("/oauth2/auth/requests/login/reject?login_challenge=#{challenge}") - |> HTTPoison.put("{}", headers()) + |> Req.put(req_opts(body: "{}")) |> handle_response(%Response{}) end def get_consent(challenge) do admin_url("/oauth2/auth/requests/consent?consent_challenge=#{challenge}") - |> HTTPoison.get(headers()) + |> Req.get(req_opts()) |> handle_response(%ConsentRequest{client: %Client{}}) end @@ -114,17 +114,19 @@ defmodule Console.Hydra.Client do } }) admin_url("/oauth2/auth/requests/consent/accept?consent_challenge=#{challenge}") - |> HTTPoison.put(body, headers()) + |> Req.put(req_opts(body: body)) |> handle_response(%Response{}) end def reject_consent(challenge) do admin_url("/oauth2/auth/requests/consent/accept?consent_challenge=#{challenge}") - |> HTTPoison.put("{}", headers()) + |> Req.put(req_opts(body: "{}")) |> handle_response(%Response{}) end - defp handle_response({:ok, %{status_code: code, body: body}}, type) when code in 200..299, + defp req_opts(extra \\ []), do: [headers: headers(), decode_body: false, retry: false] ++ extra + + defp handle_response({:ok, %{status: code, body: body}}, type) when code in 200..299, do: {:ok, Poison.decode!(body, as: type)} defp handle_response(error, _) do Logger.error "Failed to call hydra: #{inspect(error)}" @@ -155,7 +157,7 @@ defmodule Console.Hydra.Client do defp conf(key), do: Console.conf(__MODULE__)[key] defp hydra_error({:error, _}), do: "internal network error" - defp hydra_error({:ok, %HTTPoison.Response{status_code: code, body: body}}), + defp hydra_error({:ok, %Req.Response{status: code, body: body}}), do: "hydra error: code=#{code}, body=#{body}" defp headers(), do: [{"accept", "application/json"}, {"content-type", "application/json"}] diff --git a/lib/console/clients/loki/client.ex b/lib/console/clients/loki/client.ex index 629c8c1213..da51223c0b 100644 --- a/lib/console/clients/loki/client.ex +++ b/lib/console/clients/loki/client.ex @@ -17,9 +17,9 @@ defmodule Loki.Client do host(client) |> Path.join("/loki/api/v1/query_range?#{query}") - |> HTTPoison.get(Enum.uniq_by(headers() ++ auth(client), &elem(&1, 0))) + |> Req.get(headers: Enum.uniq_by(headers() ++ auth(client), &elem(&1, 0)), decode_body: false, retry: false) |> case do - {:ok, %{body: body, status_code: 200}} -> + {:ok, %{body: body, status: 200}} -> {:ok, body |> Poison.decode(as: %Response{data: %Data{result: [%Result{}]}}) |> convert()} diff --git a/lib/console/clients/prometheus/client.ex b/lib/console/clients/prometheus/client.ex index 057a7f671f..68dfb7a5aa 100644 --- a/lib/console/clients/prometheus/client.ex +++ b/lib/console/clients/prometheus/client.ex @@ -4,7 +4,7 @@ defmodule Prometheus.Client do require Logger @headers [{"content-type", "application/x-www-form-urlencoded"}] - @timeouts [timeout: :timer.seconds(30), recv_timeout: :timer.seconds(30)] + @timeouts [connect_options: [timeout: :timer.seconds(30)], receive_timeout: :timer.seconds(30), decode_body: false, retry: false] defstruct [:host, :user, :password] @@ -22,9 +22,9 @@ defmodule Prometheus.Client do query = variable_subst(query, variables) Path.join(host(client), "/api/v1/query") - |> HTTPoison.post({:form, [{"query", query}]}, @headers ++ auth(client), @timeouts) + |> Req.post([form: [{"query", query}], headers: @headers ++ auth(client)] ++ @timeouts) |> case do - {:ok, %{body: body, status_code: 200}} -> Poison.decode(body, as: Response.spec()) + {:ok, %{body: body, status: 200}} -> Poison.decode(body, as: Response.spec()) _ -> {:error, "prometheus error"} end end @@ -32,18 +32,18 @@ defmodule Prometheus.Client do def query(client \\ nil, query, start, end_t, step, variables) do query = variable_subst(query, variables) Logger.info "Issuing prometheus query: #{query}" - HTTPoison.post( + Req.post( Path.join(host(client), "/api/v1/query_range"), - {:form, [ + [form: [ {"query", query}, {"end", DateTime.to_iso8601(end_t)}, {"start", DateTime.to_iso8601(start)}, {"step", step} - ]}, - @headers ++ auth(client) + ], + headers: @headers ++ auth(client)] ++ @timeouts ) |> case do - {:ok, %{body: body, status_code: 200}} -> Poison.decode(body, as: Response.spec()) + {:ok, %{body: body, status: 200}} -> Poison.decode(body, as: Response.spec()) _ -> {:error, "prometheus error"} end end diff --git a/lib/console/deployments/clusters.ex b/lib/console/deployments/clusters.ex index 5536534b60..bf0218772c 100644 --- a/lib/console/deployments/clusters.ex +++ b/lib/console/deployments/clusters.ex @@ -1262,9 +1262,9 @@ defmodule Console.Deployments.Clusters do defp readme_fetch(url) do Enum.find_value(~w(main master), {:ok, nil}, fn branch -> String.replace(url, "{branch}", branch) - |> HTTPoison.get([], follow_redirect: true) + |> Req.get(redirect: true, decode_body: false, retry: false) |> case do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> {:ok, body} + {:ok, %Req.Response{status: 200, body: body}} -> {:ok, body} _ -> nil end end) diff --git a/lib/console/deployments/compatibilities/cloud_addons.ex b/lib/console/deployments/compatibilities/cloud_addons.ex index e715493d2d..e3cdea006a 100644 --- a/lib/console/deployments/compatibilities/cloud_addons.ex +++ b/lib/console/deployments/compatibilities/cloud_addons.ex @@ -65,14 +65,14 @@ defmodule Console.Deployments.Compatibilities.CloudAddOns do end defp fetch_manifest(url) do - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url <> "manifest.yaml"), + with {:ok, %Req.Response{status: 200, body: body}} <- Req.get(url <> "manifest.yaml", decode_body: false, retry: false), {:ok, %{"platforms" => platforms}} <- YamlElixir.read_from_string(body), do: platforms end defp fetch_platform(url, platform) do - case HTTPoison.get(url <> "#{platform}.yaml") do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + case Req.get(url <> "#{platform}.yaml", decode_body: false, retry: false) do + {:ok, %Req.Response{status: 200, body: body}} -> decode_cloud_addon(platform, body) _ -> {:error, "failed to fetch platform #{platform}"} diff --git a/lib/console/deployments/compatibilities/table.ex b/lib/console/deployments/compatibilities/table.ex index 6e18cfffcf..d713ed4bb9 100644 --- a/lib/console/deployments/compatibilities/table.ex +++ b/lib/console/deployments/compatibilities/table.ex @@ -90,7 +90,7 @@ defmodule Console.Deployments.Compatibilities.Table do end defp fetch_addons(url) do - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url, [], proxy_config()), + with {:ok, %Req.Response{status: 200, body: body}} <- Req.get(url, [decode_body: false, retry: false] ++ Console.Utils.HTTP.req_options(proxy_config())), {:ok, %{"addons" => addons}} <- YamlElixir.read_from_string(body), do: addons end diff --git a/lib/console/deployments/deprecations/table.ex b/lib/console/deployments/deprecations/table.ex index 7381dbea0e..b7f0d5e46e 100644 --- a/lib/console/deployments/deprecations/table.ex +++ b/lib/console/deployments/deprecations/table.ex @@ -57,7 +57,7 @@ defmodule Console.Deployments.Deprecations.Table do end defp fetch_and_parse(url) do - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url, [], proxy_config()), + with {:ok, %Req.Response{status: 200, body: body}} <- Req.get(url, [decode_body: false, retry: false] ++ Console.Utils.HTTP.req_options(proxy_config())), {:ok, %{"deprecated-versions" => deprecated}} <- YamlElixir.read_from_string(body) do Enum.map(deprecated, &to_entry/1) end diff --git a/lib/console/deployments/helm/repository.ex b/lib/console/deployments/helm/repository.ex index be09f9b28b..8a053f0f08 100644 --- a/lib/console/deployments/helm/repository.ex +++ b/lib/console/deployments/helm/repository.ex @@ -29,7 +29,7 @@ defmodule Console.Deployments.Helm.Repository do def charts(%HelmRepository{status: %HelmRepository.Status{ artifact: %HelmRepository.Status.Artifact{url: url} }}) when is_binary(url) do - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url), + with {:ok, %Req.Response{status: 200, body: body}} <- Req.get(url, decode_body: false, retry: false), {:ok, yaml} <- YamlElixir.read_from_string(body) do helm = %Schema{entries: yaml["entries"]} helm = Schema.transform(helm) diff --git a/lib/console/deployments/kube_versions/table.ex b/lib/console/deployments/kube_versions/table.ex index a2e0b1523b..a460022ed9 100644 --- a/lib/console/deployments/kube_versions/table.ex +++ b/lib/console/deployments/kube_versions/table.ex @@ -138,7 +138,7 @@ defmodule Console.Deployments.KubeVersions.Table do end def handle_info(:poll, %State{table: table, static: false, url: url} = state) do - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url), + with {:ok, %Req.Response{status: 200, body: body}} <- Req.get(url, decode_body: false, retry: false), {:ok, %{"kube_changelog" => changelog}} <- YamlElixir.read_from_string(body) do table = Enum.reduce(changelog, table, fn change, table -> changelog = Changelog.new(change) diff --git a/lib/console/deployments/metrics/provider/datadog.ex b/lib/console/deployments/metrics/provider/datadog.ex index 7a097797d0..41ff0cb35c 100644 --- a/lib/console/deployments/metrics/provider/datadog.ex +++ b/lib/console/deployments/metrics/provider/datadog.ex @@ -31,15 +31,15 @@ defmodule Console.Deployments.Metrics.Provider.Datadog do end defp get(conn, url) do - HTTPoison.get("#{conn.host}#{url}", Connection.headers(conn)) + Req.get("#{conn.host}#{url}", headers: Connection.headers(conn), decode_body: false, retry: false) |> handle_response() end defp listify(l) when is_list(l), do: l defp listify(v), do: [v] - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) + defp handle_response({:ok, %Req.Response{status: code, body: body}}) when code >= 200 and code < 300, do: Jason.decode(body) - defp handle_response({:ok, %HTTPoison.Response{body: body}}), do: {:error, {:client, "datadog api call failed: #{body}"}} + defp handle_response({:ok, %Req.Response{body: body}}), do: {:error, {:client, "datadog api call failed: #{body}"}} defp handle_response(_), do: {:error, {:client, "unknown datadog error"}} end diff --git a/lib/console/deployments/notifications.ex b/lib/console/deployments/notifications.ex index 1a4397d7bc..9fc689303d 100644 --- a/lib/console/deployments/notifications.ex +++ b/lib/console/deployments/notifications.ex @@ -171,10 +171,15 @@ defmodule Console.Deployments.Notifications do end defp url_deliver(url, body) do - HTTPoison.post(url, body, [ - {"content-type", "application/json"}, - {"accept", "application/json"} - ]) + Req.post(url, + headers: [ + {"content-type", "application/json"}, + {"accept", "application/json"} + ], + body: body, + decode_body: false, + retry: false + ) |> log_errors() end @@ -183,7 +188,7 @@ defmodule Console.Deployments.Notifications do |> EEx.eval_file(assigns: Map.to_list(map)) end - defp log_errors({:ok, %HTTPoison.Response{status_code: c, body: b}}) when is_integer(c) and (c < 200 or c >= 300) do + defp log_errors({:ok, %Req.Response{status: c, body: b}}) when is_integer(c) and (c < 200 or c >= 300) do Logger.error "Failed to deliver incoming webhook: #{b}" end defp log_errors(pass), do: pass diff --git a/lib/console/deployments/pr/governance/impl/webhook.ex b/lib/console/deployments/pr/governance/impl/webhook.ex index d9261a863f..33bfc1a42f 100644 --- a/lib/console/deployments/pr/governance/impl/webhook.ex +++ b/lib/console/deployments/pr/governance/impl/webhook.ex @@ -30,12 +30,13 @@ defmodule Console.Deployments.Pr.Governance.Impl.Webhook do defp make_request(%PrGovernance{configuration: %{webhook: %{url: url}}}, path, body) do Path.join(url, path) - |> HTTPoison.post(body, @headers) + |> Req.post(headers: @headers, body: body, decode_body: false, retry: false) |> case do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code in 200..299 -> + {:ok, %Req.Response{status: code, body: body}} when code in 200..299 -> Jason.decode(body) - {:ok, %HTTPoison.Response{body: body}} -> {:error, body} - {:error, %HTTPoison.Error{reason: reason}} -> {:error, reason} + {:ok, %Req.Response{body: body}} -> {:error, body} + {:error, %Req.TransportError{reason: reason}} -> {:error, reason} + {:error, error} -> {:error, error} end end end diff --git a/lib/console/deployments/pr/impl/azure.ex b/lib/console/deployments/pr/impl/azure.ex index 009daad655..504ef6ed89 100644 --- a/lib/console/deployments/pr/impl/azure.ex +++ b/lib/console/deployments/pr/impl/azure.ex @@ -143,19 +143,19 @@ defmodule Console.Deployments.Pr.Impl.Azure do defp get(conn, url) do url(conn, url) - |> HTTPoison.get(Connection.headers(conn)) + |> Req.get(headers: Connection.headers(conn), decode_body: false, retry: false) |> handle_response() end defp post(conn, url, body) do url(conn, url) - |> HTTPoison.post(Jason.encode!(body), Connection.headers(conn)) + |> Req.post(headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end defp patch(conn, url, body) do url(conn, url) - |> HTTPoison.patch(Jason.encode!(body), Connection.headers(conn)) + |> Req.patch(headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end @@ -230,9 +230,9 @@ defmodule Console.Deployments.Pr.Impl.Azure do end end - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) + defp handle_response({:ok, %Req.Response{status: code, body: body}}) when code >= 200 and code < 300, do: Jason.decode(body) - defp handle_response({:ok, %HTTPoison.Response{body: body}}), do: {:error, "azure devops request failed: #{body}"} + defp handle_response({:ok, %Req.Response{body: body}}), do: {:error, "azure devops request failed: #{body}"} defp handle_response(_), do: {:error, "unknown azure devops error"} defp connection(%PrAutomation{connection: %ScmConnection{} = conn}), do: connection(conn) diff --git a/lib/console/deployments/pr/impl/bitbucket.ex b/lib/console/deployments/pr/impl/bitbucket.ex index 8d166136b7..a47815a3a4 100644 --- a/lib/console/deployments/pr/impl/bitbucket.ex +++ b/lib/console/deployments/pr/impl/bitbucket.ex @@ -144,38 +144,38 @@ defmodule Console.Deployments.Pr.Impl.BitBucket do def merge(_, _), do: :ok defp post(conn, url, body) do - HTTPoison.post("#{conn.host}#{url}", Jason.encode!(body), Connection.headers(conn)) + Req.post("#{conn.host}#{url}", headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end defp put(conn, url, body) do - HTTPoison.put("#{conn.host}#{url}", Jason.encode!(body), Connection.headers(conn)) + Req.put("#{conn.host}#{url}", headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end defp get(%Connection{} = conn, url) do - HTTPoison.get("#{conn.host}#{url}", Connection.headers(conn)) + Req.get("#{conn.host}#{url}", headers: Connection.headers(conn), decode_body: false, retry: false) |> handle_response() end defp get(url, headers) when is_binary(url) and is_list(headers) do - HTTPoison.get(url, headers) + Req.get(url, headers: headers, decode_body: false, retry: false) |> handle_response() end defp get_raw(url, headers) when is_binary(url) and is_list(headers) do - HTTPoison.get(url, headers) + Req.get(url, headers: headers, decode_body: false, retry: false) |> handle_response_raw() end - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) + defp handle_response({:ok, %Req.Response{status: code, body: body}}) when code >= 200 and code < 300, do: Jason.decode(body) - defp handle_response({:ok, %HTTPoison.Response{body: body}}), do: {:error, "bitbucket request failed: #{body}"} + defp handle_response({:ok, %Req.Response{body: body}}), do: {:error, "bitbucket request failed: #{body}"} defp handle_response(_), do: {:error, "unknown bitbucket error"} - defp handle_response_raw({:ok, %HTTPoison.Response{status_code: code, body: body}}) + defp handle_response_raw({:ok, %Req.Response{status: code, body: body}}) when code >= 200 and code < 300, do: {:ok, body} - defp handle_response_raw({:ok, %HTTPoison.Response{body: body}}), do: {:error, "bitbucket request failed: #{body}"} + defp handle_response_raw({:ok, %Req.Response{body: body}}), do: {:error, "bitbucket request failed: #{body}"} defp handle_response_raw(_), do: {:error, "unknown bitbucket error"} defp state(%{"state" => "MERGED"}), do: :merged @@ -210,7 +210,7 @@ defmodule Console.Deployments.Pr.Impl.BitBucket do } = pr_body} <- get(conn, "/repositories/#{workspace}/#{repo}/pullrequests/#{pr_id}") do {:ok, pr_body, diff_url, diffstat_url} else - {:error, %HTTPoison.Error{reason: reason}} -> + {:error, %Req.TransportError{reason: reason}} -> {:error, "HTTP request failed: #{inspect(reason)}"} {:error, %Jason.DecodeError{}} -> {:error, "Invalid JSON response"} diff --git a/lib/console/deployments/pr/impl/bitbucket_datacenter.ex b/lib/console/deployments/pr/impl/bitbucket_datacenter.ex index e3a271d322..bc5f6c658d 100644 --- a/lib/console/deployments/pr/impl/bitbucket_datacenter.ex +++ b/lib/console/deployments/pr/impl/bitbucket_datacenter.ex @@ -177,19 +177,19 @@ defmodule Console.Deployments.Pr.Impl.BitBucketDatacenter do defp post(conn, path, body) do url(conn, path) - |> HTTPoison.post(Jason.encode!(body), Connection.headers(conn)) + |> Req.post(headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end defp put(conn, path, body) do url(conn, path) - |> HTTPoison.put(Jason.encode!(body), Connection.headers(conn)) + |> Req.put(headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) + defp handle_response({:ok, %Req.Response{status: code, body: body}}) when code >= 200 and code < 300, do: Jason.decode(body) - defp handle_response({:ok, %HTTPoison.Response{body: body}}), do: {:error, "bitbucket request failed: #{body}"} + defp handle_response({:ok, %Req.Response{body: body}}), do: {:error, "bitbucket request failed: #{body}"} defp handle_response(_), do: {:error, "unknown bitbucket error"} defp state(%{"state" => "MERGED"}), do: :merged diff --git a/lib/console/deployments/pr/impl/github.ex b/lib/console/deployments/pr/impl/github.ex index 717b0f6f32..becb04f6a3 100644 --- a/lib/console/deployments/pr/impl/github.ex +++ b/lib/console/deployments/pr/impl/github.ex @@ -185,7 +185,7 @@ defmodule Console.Deployments.Pr.Impl.Github do defp get_content(%Tentacat.Client{auth: auth, request_options: opts}, url) when is_binary(url) do headers = [{"authorization", "Token #{auth.access_token}"}] - with {:ok, %HTTPoison.Response{status_code: 200, body: content}} <- HTTPoison.get(url, headers, opts || []), + with {:ok, %Req.Response{status: 200, body: content}} <- Req.get(url, [headers: headers, decode_body: false, retry: false] ++ Console.Utils.HTTP.req_options(opts || [])), {:ok, %{"content" => content}} <- Jason.decode(content) do String.split(content) |> Enum.map(fn line -> diff --git a/lib/console/deployments/pr/impl/gitlab.ex b/lib/console/deployments/pr/impl/gitlab.ex index 6be4329ec1..faa4a56741 100644 --- a/lib/console/deployments/pr/impl/gitlab.ex +++ b/lib/console/deployments/pr/impl/gitlab.ex @@ -165,26 +165,26 @@ defmodule Console.Deployments.Pr.Impl.Gitlab do defp post(conn, url, body) do api_url(conn, url) - |> HTTPoison.post(Jason.encode!(body), Connection.headers(conn)) + |> Req.post(headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end defp put(conn, url, body) do api_url(conn, url) - |> HTTPoison.put(Jason.encode!(body), Connection.headers(conn)) + |> Req.put(headers: Connection.headers(conn), body: Jason.encode!(body), decode_body: false, retry: false) |> handle_response() end defp get(conn, url) do api_url(conn, url) - |> HTTPoison.get(Connection.headers(conn)) + |> Req.get(headers: Connection.headers(conn), decode_body: false, retry: false) |> handle_response() end - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) + defp handle_response({:ok, %Req.Response{status: code, body: body}}) when code >= 200 and code < 300, do: Jason.decode(body) - defp handle_response({:ok, %HTTPoison.Response{body: body}}), do: {:error, "gitlab request failed: #{body}"} - defp handle_response({:error, %HTTPoison.Error{reason: reason}}), do: {:error, "gitlab request failed: #{reason}"} + defp handle_response({:ok, %Req.Response{body: body}}), do: {:error, "gitlab request failed: #{body}"} + defp handle_response({:error, %Req.TransportError{reason: reason}}), do: {:error, "gitlab request failed: #{inspect(reason)}"} defp handle_response(_), do: {:error, "unknown gitlab error"} defp state(%{"state" => "merged"}), do: :merged diff --git a/lib/console/logs/provider/elastic.ex b/lib/console/logs/provider/elastic.ex index bd80aabd89..04cbab63cf 100644 --- a/lib/console/logs/provider/elastic.ex +++ b/lib/console/logs/provider/elastic.ex @@ -9,7 +9,7 @@ defmodule Console.Logs.Provider.Elastic do @type t :: %__MODULE__{} @headers [{"Content-Type", "application/json"}] - @opts [recv_timeout: :timer.seconds(30)] + @opts [receive_timeout: :timer.seconds(30), decode_body: false, retry: false] defstruct [:connection, :client] @@ -49,15 +49,15 @@ defmodule Console.Logs.Provider.Elastic do def search(%Elastic{index: index} = conn, query) do Elastic.url(conn, "#{index}/_search") - |> HTTPoison.post(Jason.encode!(query), Elastic.headers(conn, @headers), @opts) + |> Req.post([headers: Elastic.headers(conn, @headers), body: Jason.encode!(query)] ++ @opts) |> search_response() end - defp search_response({:ok, %HTTPoison.Response{status_code: 200, body: body}}) do + defp search_response({:ok, %Req.Response{status: 200, body: body}}) do with {:ok, resp} <- Jason.decode(body), do: {:ok, Snap.SearchResponse.new(resp)} end - defp search_response({:ok, %HTTPoison.Response{body: body}}), do: {:error, "es failure: #{body}"} + defp search_response({:ok, %Req.Response{body: body}}), do: {:error, "es failure: #{body}"} defp search_response(_), do: {:error, "network failure"} defp format_hits(%Snap.SearchResponse{hits: %Snap.Hits{hits: hits}}) do diff --git a/lib/console/logs/provider/victoria.ex b/lib/console/logs/provider/victoria.ex index 57b644dee1..470a959cf9 100644 --- a/lib/console/logs/provider/victoria.ex +++ b/lib/console/logs/provider/victoria.ex @@ -7,7 +7,7 @@ defmodule Console.Logs.Provider.Victoria do alias Console.Logs.{Query, Time, Line, Stream.Exec} alias Console.Schema.{Cluster, Service, DeploymentSettings.Connection} - @options [recv_timeout: :timer.seconds(30), timeout: :timer.seconds(30)] + @options [receive_timeout: :timer.seconds(30), connect_options: [timeout: :timer.seconds(30)], retry: false] @headers [{"Content-Type", "application/x-www-form-urlencoded"}] defstruct [:connection] @@ -17,10 +17,14 @@ defmodule Console.Logs.Provider.Victoria do def query(%__MODULE__{connection: %Connection{host: host} = conn}, %Query{} = query) when is_binary(host) do Exec.exec(fn -> Connection.url(conn, "/select/logsql/query") - |> HTTPoison.post({:form, [ - {"query", build_query(query)}, - {"limit", "#{Query.limit(query)}"} - ]}, Connection.headers(conn, @headers), [stream_to: self(), async: :once] ++ @options) + |> Req.post([ + form: [ + {"query", build_query(query)}, + {"limit", "#{Query.limit(query)}"} + ], + headers: Connection.headers(conn, @headers), + into: :self + ] ++ @options) end, mapper: &line/1) end def query(_, _), do: {:error, "no victoria metrics host specified"} diff --git a/lib/console/logs/stream/exec.ex b/lib/console/logs/stream/exec.ex index eb9b1d5e9a..7378530bba 100644 --- a/lib/console/logs/stream/exec.ex +++ b/lib/console/logs/stream/exec.ex @@ -22,40 +22,44 @@ defmodule Console.Logs.Stream.Exec do Stream.resource( start, fn - {:error, %HTTPoison.Error{} = error} -> {[{:error, error}], :error} - {{:error, err}, _} -> {[{:error, err}], :error} + {:error, error} -> {[{:error, error}], :error} - {:ok, %HTTPoison.AsyncResponse{}} = resp -> {[], {resp, ""}} + {:ok, %Req.Response{status: code}} = resp when code >= 200 and code < 400 -> + {[], {resp, ""}} - {{:ok, %HTTPoison.AsyncResponse{id: id} = res}, acc} -> + {:ok, %Req.Response{status: code}} -> + {[{:error, "error code: #{code}"}], :error} + + {{:ok, %Req.Response{body: %Req.Response.Async{ref: ref}} = res}, acc} -> receive do - %HTTPoison.AsyncStatus{id: ^id, code: code} when code >= 200 and code < 400 -> - {[], stream_next(res, acc)} + {^ref, _} = message -> + case Req.parse_message(res, message) do + {:ok, [data: chunk]} -> + {items, remaining} = parser.parse(acc <> chunk) + {items, {{:ok, res}, remaining}} - %HTTPoison.AsyncStatus{id: ^id, code: code} -> - {[{:error, "error code: #{code}"}], :error} + {:ok, [trailers: _]} -> + {[], {{:ok, res}, acc}} - %HTTPoison.AsyncHeaders{id: ^id, headers: _headers} -> - {[], stream_next(res, acc)} + {:ok, [:done]} -> + {:halt, res} - %HTTPoison.AsyncChunk{chunk: chunk} -> - {items, remaining} = parser.parse(acc <> chunk) - {items, stream_next(res, remaining)} + {:error, err} -> + {[{:error, err}], :error} - %HTTPoison.AsyncEnd{id: ^id} -> - {:halt, res} + :unknown -> + {[], {{:ok, res}, acc}} + end after @timeout -> {:halt, res} end - {:error, _} -> {:halt, :error} + :error -> {:halt, :error} end, fn - %{id: id} -> :hackney.stop_async(id) - :error -> :ok + %Req.Response{body: %Req.Response.Async{}} = res -> Req.cancel_async_response(res) + _ -> :ok end ) end - - defp stream_next(resp, acc), do: {HTTPoison.stream_next(resp), acc} end diff --git a/lib/console/mesh/prometheus.ex b/lib/console/mesh/prometheus.ex index fd6dbf4daf..a925d501ba 100644 --- a/lib/console/mesh/prometheus.ex +++ b/lib/console/mesh/prometheus.ex @@ -9,9 +9,9 @@ defmodule Console.Mesh.Prometheus do def query(conn, query, opts \\ []) do Path.join(conn.host, "/api/v1/query") - |> HTTPoison.post({:form, form([{"query", query}], Map.new(opts))}, headers(conn)) + |> Req.post(form: form([{"query", query}], Map.new(opts)), headers: headers(conn), decode_body: false, retry: false) |> case do - {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> + {:ok, %Req.Response{body: body, status: 200}} -> Poison.decode(body, as: Response.spec()) _ -> {:error, "prometheus error"} end diff --git a/lib/console/plural/client.ex b/lib/console/plural/client.ex index 5622a9fa90..501e6dd3f7 100644 --- a/lib/console/plural/client.ex +++ b/lib/console/plural/client.ex @@ -10,10 +10,12 @@ defmodule Console.Plural.Client do def run(query, variables, type_spec) do token = Config.fetch() - HTTPoison.post(url(), Jason.encode!(%{ - query: query, - variables: variables - }), [{"authorization", "Bearer #{token}"} | @headers]) + Req.post(url(), + headers: [{"authorization", "Bearer #{token}"} | @headers], + body: Jason.encode!(%{query: query, variables: variables}), + decode_body: false, + retry: false + ) |> decode(type_spec) end diff --git a/lib/console/utils/http.ex b/lib/console/utils/http.ex new file mode 100644 index 0000000000..a955a29fdd --- /dev/null +++ b/lib/console/utils/http.ex @@ -0,0 +1,58 @@ +defmodule Console.Utils.HTTP do + @moduledoc """ + Helpers for translating legacy HTTPoison-style request options into `Req` + options. Some option sources (eg. SCM connection proxy settings, Tentacat + request options) are still expressed in HTTPoison terms because they're shared + with dependencies that continue to use HTTPoison. This module normalizes them + for our own `Req` based clients. + """ + + @doc """ + Translates a keyword list of HTTPoison-style request options into the + equivalent `Req` options. Unknown/unsupported keys are dropped since `Req` + raises on unregistered options. + """ + @spec req_options(keyword) :: keyword + def req_options(opts) when is_list(opts) do + Enum.reduce(opts, [], fn + {:proxy, url}, acc when is_binary(url) -> + merge_connect(acc, proxy: parse_proxy(url)) + + {:proxy, {_, _, _, _} = proxy}, acc -> + merge_connect(acc, proxy: proxy) + + {:recv_timeout, t}, acc -> + Keyword.put(acc, :receive_timeout, t) + + {:timeout, t}, acc -> + merge_connect(acc, timeout: t) + + {:ssl, ssl}, acc when is_list(ssl) -> + merge_connect(acc, transport_opts: ssl) + + {:follow_redirect, follow?}, acc -> + Keyword.put(acc, :redirect, follow?) + + {:max_redirect, max}, acc -> + Keyword.put(acc, :max_redirects, max) + + _, acc -> + acc + end) + end + + def req_options(_), do: [] + + defp merge_connect(opts, connect) do + Keyword.update(opts, :connect_options, connect, &Keyword.merge(&1, connect)) + end + + defp parse_proxy(url) do + uri = URI.parse(url) + scheme = if uri.scheme == "https", do: :https, else: :http + {scheme, uri.host, uri.port || default_port(scheme), []} + end + + defp default_port(:https), do: 443 + defp default_port(_), do: 80 +end diff --git a/lib/console_web/controllers/ai_controller.ex b/lib/console_web/controllers/ai_controller.ex index 7ad6879cad..e45169f9b4 100644 --- a/lib/console_web/controllers/ai_controller.ex +++ b/lib/console_web/controllers/ai_controller.ex @@ -4,7 +4,9 @@ defmodule ConsoleWeb.AIController do alias Console.Schema.Cluster alias Console.Deployments.Agents - @options [recv_timeout: :timer.minutes(5), timeout: :timer.minutes(5)] + @options [receive_timeout: :timer.minutes(5), connect_options: [timeout: :timer.minutes(5)]] + @stream_timeout :timer.minutes(5) + @hop_by_hop_headers ~w(connection content-length keep-alive proxy-authenticate proxy-authorization te trailer transfer-encoding upgrade) plug :verify @@ -44,43 +46,50 @@ defmodule ConsoleWeb.AIController do url = find_uri(upstream, conn.query_string) - case HTTPoison.post( - url, - Enum.reverse(body) |> IO.iodata_to_binary(), - convert_headers(conn, upstream), - [stream_to: self(), async: :once] ++ @options - ) do - {:ok, %HTTPoison.AsyncResponse{} = resp} -> - do_stream(conn, resp) + case Req.post(url, [ + body: Enum.reverse(body) |> IO.iodata_to_binary(), + headers: convert_headers(conn, upstream), + into: :self, + decode_body: false, + retry: false + ] ++ @options) do + {:ok, %Req.Response{} = resp} -> + start_stream(conn, resp) {:error, _} -> send_resp(conn, 500, "error proxying request") end end - defp do_stream(conn, %HTTPoison.AsyncResponse{} = resp) do - with {:ok, resp} <- HTTPoison.stream_next(resp) do - receive do - %HTTPoison.AsyncStatus{code: code} -> - put_status(conn, code) - |> do_stream(resp) - - %HTTPoison.AsyncHeaders{headers: headers} -> - Enum.reduce(headers, conn, fn {k, v}, conn -> - put_resp_header(conn, String.downcase(k), v) - end) - |> send_chunked(conn.status) - |> do_stream(resp) - - %HTTPoison.AsyncChunk{chunk: chunk} -> - case chunk(conn, chunk) do - {:ok, conn} -> do_stream(conn, resp) - {:error, _} -> conn - end - - %HTTPoison.AsyncEnd{} -> conn - end - else - _ -> send_resp(conn, 500, "error streaming response") + defp start_stream(conn, %Req.Response{status: status, headers: headers} = resp) do + Enum.reduce(headers, put_status(conn, status), fn + {k, values}, conn when is_list(values) -> put_upstream_header(conn, String.downcase(k), values) + {k, value}, conn -> put_upstream_header(conn, String.downcase(k), [value]) + end) + |> send_chunked(status) + |> do_stream(resp) + end + + defp put_upstream_header(conn, name, _values) when name in @hop_by_hop_headers, do: conn + defp put_upstream_header(conn, name, values), do: put_resp_header(conn, name, Enum.join(values, ", ")) + + defp do_stream(conn, %Req.Response{body: %Req.Response.Async{ref: ref}} = resp) do + receive do + {^ref, {:data, chunk}} -> + case chunk(conn, chunk) do + {:ok, conn} -> do_stream(conn, resp) + {:error, _} -> conn + end + + {^ref, {:trailers, _}} -> + do_stream(conn, resp) + + {^ref, :done} -> conn + + {^ref, {:error, _}} -> conn + after + @stream_timeout -> + Req.cancel_async_response(resp) + conn end end diff --git a/lib/mix/tasks/db.certs.ex b/lib/mix/tasks/db.certs.ex index 065c484a62..df82eab928 100644 --- a/lib/mix/tasks/db.certs.ex +++ b/lib/mix/tasks/db.certs.ex @@ -1,6 +1,6 @@ defmodule Mix.Tasks.Db.Certs do require Logger - @deps ~w(logger httpoison hackney)a + @deps ~w(logger req)a @urls [ aws: ~w(https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem), @@ -25,10 +25,10 @@ defmodule Mix.Tasks.Db.Certs do defp build_pem(path, scope, urls) do Enum.reduce(urls, [], fn url, acc -> - case HTTPoison.get(url) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + case Req.get(url, raw: true, retry: false) do + {:ok, %Req.Response{status: 200, body: body}} -> [parse(body, url) | acc] - {:ok, %HTTPoison.Response{body: body}} -> + {:ok, %Req.Response{body: body}} -> Logger.info("Error fetching RDS CA: #{inspect(body)}") acc {:error, error} -> diff --git a/lib/mix/tasks/elasticsearch/up.ex b/lib/mix/tasks/elasticsearch/up.ex index f132433e4c..547d123b88 100644 --- a/lib/mix/tasks/elasticsearch/up.ex +++ b/lib/mix/tasks/elasticsearch/up.ex @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Elasticsearch.Up do @index Application.compile_env(:elasticsearch, :index) def run(_) do - HTTPoison.start() + {:ok, _} = Application.ensure_all_started(:req) with true <- index_exists?() do IO.puts("Elasticsearch index #{@index} already exists at #{@host}. Will delete") delete_index() @@ -27,7 +27,7 @@ defmodule Mix.Tasks.Elasticsearch.Up do def create_index(index \\ @index) do url("/#{index}") - |> HTTPoison.put!("", []) + |> Req.put!(body: "", decode_body: false, retry: false) end defp url(path), do: "#{@host}#{path}" diff --git a/lib/mix/tasks/prom.mocks.ex b/lib/mix/tasks/prom.mocks.ex index 8b9df5695d..7b3b34d6f6 100644 --- a/lib/mix/tasks/prom.mocks.ex +++ b/lib/mix/tasks/prom.mocks.ex @@ -16,7 +16,7 @@ defmodule Mix.Tasks.Prom.Mocks do @headers [{"content-type", "application/x-www-form-urlencoded"}] def run(_) do - {:ok, _} = Application.ensure_all_started(:hackney) + {:ok, _} = Application.ensure_all_started(:req) conn = %Connection{ host: get_env("PROMETHEUS_HOST"), @@ -39,14 +39,14 @@ defmodule Mix.Tasks.Prom.Mocks do defp query_range(conn, query) do Path.join(conn.host, "/api/v1/query_range") - |> HTTPoison.post({:form, [ + |> Req.post(form: [ {"query", query}, {"end", @end_t}, {"start", @start_t}, {"step", "1d"} - ]}, headers(conn)) + ], headers: headers(conn), decode_body: false, retry: false) |> case do - {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> {:ok, body} + {:ok, %Req.Response{body: body, status: 200}} -> {:ok, body} _ -> {:error, "prometheus error"} end end diff --git a/test/console/ai/tools/workbench/integration/github/response_test.exs b/test/console/ai/tools/workbench/integration/github/response_test.exs index 83cb63f5c4..7883cb7caf 100644 --- a/test/console/ai/tools/workbench/integration/github/response_test.exs +++ b/test/console/ai/tools/workbench/integration/github/response_test.exs @@ -8,7 +8,7 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.ResponseTest do test "merges multi-page Tentacat search bodies before encoding" do page1 = %{"incomplete_results" => false, "items" => [%{"id" => 1}], "total_count" => 2} page2 = %{"incomplete_results" => false, "items" => [%{"id" => 2}], "total_count" => 2} - fake = %HTTPoison.Response{} + fake = %Req.Response{} assert {:ok, json} = Response.json({200, [{200, page1, fake}, {200, page2, fake}], fake}) @@ -21,7 +21,7 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.ResponseTest do test "encodes a normal single-page Tentacat body" do body = %{"incomplete_results" => false, "items" => [%{"id" => 1}]} - fake = %HTTPoison.Response{} + fake = %Req.Response{} assert {:ok, json} = Response.json({200, body, fake}) assert Jason.decode!(json) == body @@ -30,7 +30,7 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.ResponseTest do test "includes manual pagination metadata" do body = [%{"id" => 1}] next = "https://api.github.com/repos/pluralsh/console/pulls?page=2&per_page=30" - fake = %HTTPoison.Response{headers: [{"Link", "<#{next}>; rel=\"next\""}]} + fake = %Req.Response{headers: %{"link" => ["<#{next}>; rel=\"next\""]}} assert {:ok, json} = Response.json({{200, body, fake}, next, nil}) @@ -47,14 +47,14 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.ResponseTest do end test "bubbles Tentacat transport failures as tool errors" do - error = %HTTPoison.Error{ + error = %Req.TransportError{ reason: {:tls_alert, {:unknown_ca, ~c"TLS client: certificate verify failed"}} } assert {:error, message} = Response.json({:error, error}) assert message =~ "GitHub request failed: TLS unknown_ca:" assert message =~ "certificate verify failed" - refute message =~ "%HTTPoison.Error" + refute message =~ "%Req.TransportError" end end @@ -62,18 +62,16 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.ResponseTest do test "safely decodes manually paginated object responses" do next = "https://api.github.com/repos/pluralsh/console/commits/sha/check-runs?page=2&per_page=30" - expect(HTTPoison, :request, fn - :get, - "https://api.github.com/repos/pluralsh/console/commits/sha/check-runs?page=1&per_page=30", - "", - _headers, - _opts -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: Jason.encode!(%{"total_count" => 1, "check_runs" => [%{"id" => 1}]}), - headers: [{"Link", "<#{next}>; rel=\"next\""}] - }} + expect(Req, :request, fn opts -> + assert opts[:method] == :get + assert opts[:url] == "https://api.github.com/repos/pluralsh/console/commits/sha/check-runs?page=1&per_page=30" + + {:ok, + %Req.Response{ + status: 200, + body: Jason.encode!(%{"total_count" => 1, "check_runs" => [%{"id" => 1}]}), + headers: %{"link" => ["<#{next}>; rel=\"next\""]} + }} end) client = Tentacat.Client.new(%{access_token: "token"}) @@ -100,18 +98,16 @@ defmodule Console.AI.Tools.Workbench.Integration.Github.ResponseTest do end test "returns an error for non-json GitHub responses instead of raising a decoder error" do - expect(HTTPoison, :request, fn - :get, - "https://api.github.com/bad/path", - "", - _headers, - _opts -> - {:ok, - %HTTPoison.Response{ - status_code: 404, - body: "not found", - headers: [] - }} + expect(Req, :request, fn opts -> + assert opts[:method] == :get + assert opts[:url] == "https://api.github.com/bad/path" + + {:ok, + %Req.Response{ + status: 404, + body: "not found", + headers: %{} + }} end) client = Tentacat.Client.new(%{access_token: "token"}) diff --git a/test/console/ai/tools/workbench/integration/gitlab/client_test.exs b/test/console/ai/tools/workbench/integration/gitlab/client_test.exs index cf2dbbb678..02af99f596 100644 --- a/test/console/ai/tools/workbench/integration/gitlab/client_test.exs +++ b/test/console/ai/tools/workbench/integration/gitlab/client_test.exs @@ -6,9 +6,9 @@ defmodule Console.AI.Tools.Workbench.Integration.Gitlab.ClientTest do describe "get/3" do test "bubbles HTTP transport failures as string errors" do - expect(HTTPoison, :get, fn _url, _headers, _opts -> + expect(Req, :get, fn _url, _opts -> {:error, - %HTTPoison.Error{ + %Req.TransportError{ reason: {:tls_alert, {:unknown_ca, ~c"TLS client: certificate verify failed"}} }} end) diff --git a/test/console/ai/tools/workbench/integration/http_test.exs b/test/console/ai/tools/workbench/integration/http_test.exs index 8c86efb45d..fc047a3f43 100644 --- a/test/console/ai/tools/workbench/integration/http_test.exs +++ b/test/console/ai/tools/workbench/integration/http_test.exs @@ -4,8 +4,8 @@ defmodule Console.AI.Tools.Workbench.Integration.HttpTest do alias Console.AI.Tools.Workbench.Integration.Http describe "error/2" do - test "formats TLS alert errors without leaking HTTPoison structs downstream" do - reason = %HTTPoison.Error{ + test "formats TLS alert errors without leaking Req structs downstream" do + reason = %Req.TransportError{ reason: {:tls_alert, {:unknown_ca, @@ -15,11 +15,11 @@ defmodule Console.AI.Tools.Workbench.Integration.HttpTest do assert {:error, message} = Http.error("GitLab", reason) assert message =~ "GitLab request failed: TLS unknown_ca:" assert message =~ "certificate verify failed" - refute message =~ "%HTTPoison.Error" + refute message =~ "%Req.TransportError" end test "formats ordinary transport errors" do - assert Http.error("GitHub", %HTTPoison.Error{reason: :econnrefused}) == + assert Http.error("GitHub", %Req.TransportError{reason: :econnrefused}) == {:error, "GitHub request failed: econnrefused"} end end diff --git a/test/console/deployments/cron_test.exs b/test/console/deployments/cron_test.exs index 37958f4ebe..22666afd6e 100644 --- a/test/console/deployments/cron_test.exs +++ b/test/console/deployments/cron_test.exs @@ -291,9 +291,9 @@ defmodule Console.Deployments.CronTest do governance = insert(:pr_governance, configuration: %{webhook: %{url: "https://webhook.url"}}) pr = insert(:pull_request, url: "https://github.com/pluralsh/console/pull/1", governance: governance) - expect(HTTPoison, :post, fn "https://webhook.url/v1/confirm", _, _ -> + expect(Req, :post, fn "https://webhook.url/v1/confirm", _ -> body = Jason.encode!(%{state: %{service_now_id: "1234567890"}}) - {:ok, %HTTPoison.Response{status_code: 200, body: body}} + {:ok, %Req.Response{status: 200, body: body}} end) expect(Tentacat.Pulls.Reviews, :create, fn _, "pluralsh", "console", "1", _ -> diff --git a/test/console/deployments/notifications_test.exs b/test/console/deployments/notifications_test.exs index b07127a628..722f170a05 100644 --- a/test/console/deployments/notifications_test.exs +++ b/test/console/deployments/notifications_test.exs @@ -118,7 +118,7 @@ defmodule Console.Deployments.NotificationsTest do test "it can deliver a service.update event" do service = insert(:service) sink = insert(:notification_sink, type: :slack) - expect(HTTPoison, :post, fn "https://example.com", res, _ -> {:ok, res} end) + expect(Req, :post, fn "https://example.com", opts -> {:ok, opts[:body]} end) {:ok, res} = Notifications.deliver( "service.update", diff --git a/test/console/deployments/pr/impl/gitlab_test.exs b/test/console/deployments/pr/impl/gitlab_test.exs index a31e59b3e9..8a1a057363 100644 --- a/test/console/deployments/pr/impl/gitlab_test.exs +++ b/test/console/deployments/pr/impl/gitlab_test.exs @@ -74,11 +74,11 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do identifier: @nested_project } - expect(HTTPoison, :post, fn url, body, headers -> + expect(Req, :post, fn url, opts -> assert url == "https://gitlab.acme.corp/api/v4/projects/#{@nested_api_project}/merge_requests" - assert Jason.decode!(body) == %{ + assert Jason.decode!(opts[:body]) == %{ "allow_collaboration" => true, "description" => "body", "source_branch" => "feature/agent", @@ -86,11 +86,11 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do "title" => "Agent PR" } - assert {"PRIVATE-TOKEN", "token"} in headers + assert {"PRIVATE-TOKEN", "token"} in opts[:headers] {:ok, - %HTTPoison.Response{ - status_code: 201, + %Req.Response{ + status: 201, body: Jason.encode!(%{ "web_url" => @nested_mr, @@ -110,13 +110,13 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do test "get_mr_info requests use encoded project paths for simple repos" do {:ok, gl_conn} = Gitlab.Connection.new("https://gitlab.com", "token") - expect(HTTPoison, :get, fn url, _headers -> + expect(Req, :get, fn url, _opts -> assert url == "https://gitlab.com/api/v4/projects/#{@simple_api_project}/merge_requests/1/changes" {:ok, - %HTTPoison.Response{ - status_code: 200, + %Req.Response{ + status: 200, body: Jason.encode!(%{"changes" => [], "sha" => "abc"}) }} end) @@ -128,15 +128,15 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do conn = %ScmConnection{type: :gitlab, api_url: "https://gitlab.acme.corp", token: "token"} mr_url = @nested_mr - expect(HTTPoison, :get, 2, fn url, _headers -> + expect(Req, :get, 2, fn url, _opts -> cond do String.contains?(url, "/merge_requests/42/changes") -> assert url == "https://gitlab.acme.corp/api/v4/projects/#{@nested_api_project}/merge_requests/42/changes" {:ok, - %HTTPoison.Response{ - status_code: 200, + %Req.Response{ + status: 200, body: Jason.encode!(%{ "sha" => "abc123", @@ -157,8 +157,8 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do "https://gitlab.acme.corp/api/v4/projects/#{@nested_api_project}/repository/files/README.md?ref=abc123" {:ok, - %HTTPoison.Response{ - status_code: 200, + %Req.Response{ + status: 200, body: Jason.encode!(%{"content" => Base.encode64("hello")}) }} @@ -176,12 +176,12 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do conn = %ScmConnection{type: :gitlab, api_url: "https://gitlab.acme.corp", token: "token"} pr = %Console.Schema.PullRequest{url: @nested_mr} - expect(HTTPoison, :post, fn url, body, _headers -> + expect(Req, :post, fn url, opts -> assert url == "https://gitlab.acme.corp/api/v4/projects/#{@nested_api_project}/merge_requests/42/notes" - assert Jason.decode!(body) == %{"body" => "looks good"} - {:ok, %HTTPoison.Response{status_code: 201, body: Jason.encode!(%{"id" => 99})}} + assert Jason.decode!(opts[:body]) == %{"body" => "looks good"} + {:ok, %Req.Response{status: 201, body: Jason.encode!(%{"id" => 99})}} end) assert {:ok, "99"} = Gitlab.review(conn, pr, "looks good") @@ -203,13 +203,13 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do identifier: @nested_project } - expect(HTTPoison, :post, fn url, _body, _headers -> + expect(Req, :post, fn url, _opts -> assert url == "https://gitlab.acme.corp/api/v4/projects/#{@nested_api_project}/merge_requests" {:ok, - %HTTPoison.Response{ - status_code: 201, + %Req.Response{ + status: 201, body: Jason.encode!(%{ "web_url" => @nested_mr, @@ -231,13 +231,13 @@ defmodule Console.Deployments.Pr.Impl.GitlabTest do token: "token" } - expect(HTTPoison, :post, fn url, _body, _headers -> + expect(Req, :post, fn url, _opts -> assert url == "https://gitlab.acme.corp/api/v4/projects/#{@nested_api_project}/merge_requests" {:ok, - %HTTPoison.Response{ - status_code: 201, + %Req.Response{ + status: 201, body: Jason.encode!(%{ "web_url" => @nested_mr, diff --git a/test/console/deployments/pubsub/governance_test.exs b/test/console/deployments/pubsub/governance_test.exs index 944809b053..77ff5a6115 100644 --- a/test/console/deployments/pubsub/governance_test.exs +++ b/test/console/deployments/pubsub/governance_test.exs @@ -9,9 +9,9 @@ defmodule Console.Deployments.PubSub.GovernanceTest do governance = insert(:pr_governance, configuration: %{webhook: %{url: "https://webhook.url"}}) pr = insert(:pull_request, governance: governance, status: :open) - expect(HTTPoison, :post, fn "https://webhook.url/v1/open", _, _ -> + expect(Req, :post, fn "https://webhook.url/v1/open", _ -> state = Jason.encode!(%{service_now_id: "1234567890"}) - {:ok, %HTTPoison.Response{status_code: 200, body: state}} + {:ok, %Req.Response{status: 200, body: state}} end) event = %PubSub.PullRequestCreated{item: pr} @@ -26,9 +26,9 @@ defmodule Console.Deployments.PubSub.GovernanceTest do governance = insert(:pr_governance, configuration: %{webhook: %{url: "https://webhook.url"}}) pr = insert(:pull_request, governance: governance, status: :open, governance_changed: true) - expect(HTTPoison, :post, fn "https://webhook.url/v1/open", _, _ -> + expect(Req, :post, fn "https://webhook.url/v1/open", _ -> state = Jason.encode!(%{service_now_id: "1234567890"}) - {:ok, %HTTPoison.Response{status_code: 200, body: state}} + {:ok, %Req.Response{status: 200, body: state}} end) event = %PubSub.PullRequestUpdated{item: pr} @@ -41,9 +41,9 @@ defmodule Console.Deployments.PubSub.GovernanceTest do governance = insert(:pr_governance, configuration: %{webhook: %{url: "https://webhook.url"}}) pr = insert(:pull_request, governance: governance, status: :merged) - expect(HTTPoison, :post, fn "https://webhook.url/v1/close", _, _ -> + expect(Req, :post, fn "https://webhook.url/v1/close", _ -> state = Jason.encode!(%{service_now_id: "1234567890"}) - {:ok, %HTTPoison.Response{status_code: 200, body: state}} + {:ok, %Req.Response{status: 200, body: state}} end) event = %PubSub.PullRequestUpdated{item: pr} diff --git a/test/console/deployments/pubsub/notification_test.exs b/test/console/deployments/pubsub/notification_test.exs index 27246de2b1..8402b14e92 100644 --- a/test/console/deployments/pubsub/notification_test.exs +++ b/test/console/deployments/pubsub/notification_test.exs @@ -14,7 +14,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do router = insert(:notification_router, events: ["service.update"]) insert(:router_sink, router: router) insert(:router_filter, router: router, service: svc) - expect(HTTPoison, :post, fn _, _, _ -> {:ok, %HTTPoison.Response{}} end) + expect(Req, :post, fn _, _ -> {:ok, %Req.Response{}} end) event = %PubSub.ServiceUpdated{item: svc} :ok = Notifications.handle_event(event) @@ -24,7 +24,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do svc = insert(:service) router = insert(:notification_router, events: ["service.update"]) insert(:router_sink, router: router) - expect(HTTPoison, :post, fn _, _, _ -> {:ok, %HTTPoison.Response{}} end) + expect(Req, :post, fn _, _ -> {:ok, %Req.Response{}} end) event = %PubSub.ServiceUpdated{item: svc} :ok = Notifications.handle_event(event) @@ -78,7 +78,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do router = insert(:notification_router, events: ["pr.create"]) insert(:router_sink, router: router) insert(:router_filter, router: router, regex: ".*/pluralsh/console/.*") - expect(HTTPoison, :post, fn _, _, _ -> {:ok, %HTTPoison.Response{}} end) + expect(Req, :post, fn _, _ -> {:ok, %Req.Response{}} end) event = %PubSub.PullRequestCreated{item: pr} :ok = Notifications.handle_event(event) @@ -94,7 +94,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do router = insert(:notification_router, events: ["pr.close"]) insert(:router_sink, router: router) insert(:router_filter, router: router, regex: ".*/pluralsh/console/.*") - expect(HTTPoison, :post, fn _, _, _ -> {:ok, %HTTPoison.Response{}} end) + expect(Req, :post, fn _, _ -> {:ok, %Req.Response{}} end) event = %PubSub.PullRequestUpdated{item: pr} :ok = Notifications.handle_event(event) @@ -108,7 +108,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do router = insert(:notification_router, events: ["pr.close"]) insert(:router_sink, router: router) insert(:router_filter, router: router, regex: ".*/pluralsh/console/.*") - expect(HTTPoison, :post, fn _, _, _ -> {:ok, %HTTPoison.Response{}} end) + expect(Req, :post, fn _, _ -> {:ok, %Req.Response{}} end) event = %PubSub.PullRequestUpdated{item: pr} :ok = Notifications.handle_event(event) @@ -125,7 +125,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do router = insert(:notification_router, events: ["pipeline.update"]) insert(:router_sink, router: router) insert(:router_filter, router: router, pipeline: pipe) - expect(HTTPoison, :post, fn _, _, _ -> {:ok, %HTTPoison.Response{}} end) + expect(Req, :post, fn _, _ -> {:ok, %Req.Response{}} end) event = %PubSub.PipelineGateUpdated{item: gate} :ok = Notifications.handle_event(event) @@ -139,9 +139,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_sink, router: router) insert(:router_filter, router: router, stack: run.stack) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.StackRunCreated{item: run} @@ -157,7 +157,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do router = insert(:notification_router, events: ["stack.run"]) insert(:router_sink, router: router) insert(:router_filter, router: router, stack: run.stack) - reject(HTTPoison, :post, 3) + reject(Req, :post, 2) event = %PubSub.StackRunCreated{item: run} :ok = Notifications.handle_event(event) @@ -171,9 +171,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_sink, router: router) insert(:router_filter, router: router, stack: run.stack) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.StackRunUpdated{item: run} @@ -189,7 +189,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do router = insert(:notification_router, events: ["stack.pending"]) insert(:router_sink, router: router) insert(:router_filter, router: router, stack: run.stack) - reject(HTTPoison, :post, 3) + reject(Req, :post, 2) event = %PubSub.StackRunUpdated{item: run} :ok = Notifications.handle_event(event) @@ -205,9 +205,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_filter, router: router, service: svc) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.ServiceInsight{item: {svc, insight}} @@ -253,9 +253,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_filter, router: router, stack: stack) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.StackInsight{item: {stack, insight}} @@ -276,9 +276,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_filter, router: router, cluster: cluster) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.ClusterInsight{item: {cluster, insight}} @@ -298,9 +298,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_filter, router: router) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.SentinelRunUpdated{item: run} @@ -315,7 +315,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do run = insert(:sentinel_run, status: :pending) router = insert(:notification_router, events: ["sentinel.run.failed"]) insert(:router_sink, router: router) - reject(&HTTPoison.post/3) + reject(&Req.post/2) event = %PubSub.SentinelRunUpdated{item: run} :ok = Notifications.handle_event(event) @@ -331,9 +331,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_filter, router: router, cluster: cluster) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.AlertCreated{item: %{alert | state_changed: true}} @@ -352,9 +352,9 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_filter, router: router, cluster: cluster) me = self() - expect(HTTPoison, :post, fn _, body, _ -> - send me, {:body, body} - {:ok, %HTTPoison.Response{}} + expect(Req, :post, fn _, opts -> + send me, {:body, opts[:body]} + {:ok, %Req.Response{}} end) event = %PubSub.AlertCreated{item: %{alert | state_changed: true}} @@ -373,7 +373,7 @@ defmodule Console.Deployments.PubSub.NotificationsTest do insert(:router_sink, router: router) insert(:router_filter, router: router, cluster: cluster) - reject(&HTTPoison.post/3) + reject(&Req.post/2) event = %PubSub.AlertCreated{item: alert} :ok = Notifications.handle_event(event) diff --git a/test/console/features_test.exs b/test/console/features_test.exs index 0ad9f87f5c..b4ea905b83 100644 --- a/test/console/features_test.exs +++ b/test/console/features_test.exs @@ -14,7 +14,8 @@ defmodule Console.FeaturesTest do }) account = %{id: "id", availableFeatures: %{vpn: true}} - expect(HTTPoison, :post, fn _, ^body, _ -> + expect(Req, :post, fn _, opts -> + assert opts[:body] == body {:ok, %{body: Jason.encode!(%{data: %{account: account}})}} end) From d9f6367be461a0a2e1472545f76b740020a1cfc1 Mon Sep 17 00:00:00 2001 From: kinjalh Date: Wed, 29 Jul 2026 11:14:00 -0400 Subject: [PATCH 2/2] fixes --- .../workbench/integration/azure_devops/client.ex | 2 +- .../tools/workbench/integration/bitbucket/client.ex | 2 +- .../integration/bitbucket_datacenter/client.ex | 2 +- .../ai/tools/workbench/integration/gitlab/client.ex | 2 +- lib/console/utils/http.ex | 13 +++++++++++++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/console/ai/tools/workbench/integration/azure_devops/client.ex b/lib/console/ai/tools/workbench/integration/azure_devops/client.ex index 7c97febf51..0f325bf601 100644 --- a/lib/console/ai/tools/workbench/integration/azure_devops/client.ex +++ b/lib/console/ai/tools/workbench/integration/azure_devops/client.ex @@ -190,5 +190,5 @@ defmodule Console.AI.Tools.Workbench.Integration.AzureDevops.Client do defp http_opts, do: - Application.get_env(:console, :req_azure_devops_options, []) ++ [receive_timeout: 60_000, decode_body: false, retry: false] + Console.Utils.HTTP.provider_options(:httpoison_azure_devops_options, :req_azure_devops_options) ++ [receive_timeout: 60_000, decode_body: false, retry: false] end diff --git a/lib/console/ai/tools/workbench/integration/bitbucket/client.ex b/lib/console/ai/tools/workbench/integration/bitbucket/client.ex index 605f70f10f..421fee89b7 100644 --- a/lib/console/ai/tools/workbench/integration/bitbucket/client.ex +++ b/lib/console/ai/tools/workbench/integration/bitbucket/client.ex @@ -103,5 +103,5 @@ defmodule Console.AI.Tools.Workbench.Integration.Bitbucket.Client do defp enc(s) when is_binary(s), do: URI.encode(String.trim(s), &URI.char_unreserved?/1) defp http_opts, - do: Application.get_env(:console, :req_bitbucket_options, []) ++ [receive_timeout: 60_000, decode_body: false, retry: false] + do: Console.Utils.HTTP.provider_options(:httpoison_bitbucket_options, :req_bitbucket_options) ++ [receive_timeout: 60_000, decode_body: false, retry: false] end diff --git a/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex b/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex index e44e1726d9..f9f392604b 100644 --- a/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex +++ b/lib/console/ai/tools/workbench/integration/bitbucket_datacenter/client.ex @@ -154,6 +154,6 @@ defmodule Console.AI.Tools.Workbench.Integration.BitbucketDatacenter.Client do defp http_opts, do: - Application.get_env(:console, :req_bitbucket_datacenter_options, []) ++ + Console.Utils.HTTP.provider_options(:httpoison_bitbucket_datacenter_options, :req_bitbucket_datacenter_options) ++ [receive_timeout: 60_000, decode_body: false, retry: false] end diff --git a/lib/console/ai/tools/workbench/integration/gitlab/client.ex b/lib/console/ai/tools/workbench/integration/gitlab/client.ex index bb16d876e9..5dd2a1131a 100644 --- a/lib/console/ai/tools/workbench/integration/gitlab/client.ex +++ b/lib/console/ai/tools/workbench/integration/gitlab/client.ex @@ -98,7 +98,7 @@ defmodule Console.AI.Tools.Workbench.Integration.Gitlab.Client do end defp http_opts, - do: Application.get_env(:console, :req_gitlab_options, []) ++ [receive_timeout: 60_000, decode_body: false, retry: false] + do: Console.Utils.HTTP.provider_options(:httpoison_gitlab_options, :req_gitlab_options) ++ [receive_timeout: 60_000, decode_body: false, retry: false] @doc false def encode_project_id(project) when is_integer(project), do: Integer.to_string(project) diff --git a/lib/console/utils/http.ex b/lib/console/utils/http.ex index a955a29fdd..c96fd4bfbc 100644 --- a/lib/console/utils/http.ex +++ b/lib/console/utils/http.ex @@ -43,6 +43,19 @@ defmodule Console.Utils.HTTP do def req_options(_), do: [] + @doc """ + Resolves per-provider request options from application config while preserving + backwards compatibility with the legacy HTTPoison-style `legacy_key`. Any + values still configured under `legacy_key` (eg. `proxy`, `ssl`, `recv_timeout`) + are translated into their `Req` equivalents, while values under `req_key` are + treated as native `Req` options and take precedence when both are set. + """ + @spec provider_options(atom, atom) :: keyword + def provider_options(legacy_key, req_key) do + legacy = req_options(Application.get_env(:console, legacy_key, [])) + Keyword.merge(legacy, Application.get_env(:console, req_key, [])) + end + defp merge_connect(opts, connect) do Keyword.update(opts, :connect_options, connect, &Keyword.merge(&1, connect)) end