From 2bb548c77552335234f921f7c75150e1f0c7e7d2 Mon Sep 17 00:00:00 2001 From: Randy Coulman Date: Tue, 14 Jul 2026 19:25:38 -0700 Subject: [PATCH 1/7] Introduce API behavior In order to prepare for the conversion to Req, we introduce a new API behaviour to replace HTTPoison.Base as well as a default implementation that delegates to an HTTPoison implementation. This uses an extra module because `use HTTPoison.Base` causes us to implement the `HTTPoison.Base` behaviour, and Elixir doesn't allow us to implement both that and ConfigCat.API when they both define the same callback. --- lib/config_cat/api.ex | 11 +++++++++++ lib/config_cat/config_fetcher.ex | 3 +-- test/support/mocks.ex | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/config_cat/api.ex b/lib/config_cat/api.ex index 452ab857..f21c7093 100644 --- a/lib/config_cat/api.ex +++ b/lib/config_cat/api.ex @@ -1,6 +1,17 @@ defmodule ConfigCat.API do @moduledoc false + @callback get(binary(), [{binary(), binary()}], Keyword.t()) :: {:ok, struct()} | {:error, struct()} +end + +defmodule ConfigCat.API.Impl do + @moduledoc false + @behaviour ConfigCat.API + defdelegate get(url, headers, options), to: ConfigCat.API.HTTPoisonImpl +end + +defmodule ConfigCat.API.HTTPoisonImpl do + @moduledoc false use HTTPoison.Base @impl HTTPoison.Base diff --git a/lib/config_cat/config_fetcher.ex b/lib/config_cat/config_fetcher.ex index c47bd6ce..6e0340e0 100644 --- a/lib/config_cat/config_fetcher.ex +++ b/lib/config_cat/config_fetcher.ex @@ -2,7 +2,6 @@ defmodule ConfigCat.ConfigFetcher do @moduledoc false alias ConfigCat.ConfigEntry - alias HTTPoison.Response defmodule FetchError do @moduledoc false @@ -54,7 +53,7 @@ defmodule ConfigCat.CacheControlConfigFetcher do use TypedStruct typedstruct enforce: true do - field :api, module(), default: ConfigCat.API + field :api, module(), default: ConfigCat.API.Impl field :base_url, String.t() field :callers, [GenServer.from()], default: [] field :connect_timeout_milliseconds, non_neg_integer(), default: 8_000 diff --git a/test/support/mocks.ex b/test/support/mocks.ex index 6b64119b..89b33916 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -1,4 +1,4 @@ -Mox.defmock(ConfigCat.MockAPI, for: HTTPoison.Base) +Mox.defmock(ConfigCat.MockAPI, for: ConfigCat.API) Mox.defmock(ConfigCat.MockCachePolicy, for: ConfigCat.CachePolicy.Behaviour) Mox.defmock(ConfigCat.MockConfigCache, for: ConfigCat.ConfigCache) Mox.defmock(ConfigCat.MockFetcher, for: ConfigCat.ConfigFetcher) From 11bf2cb3765ef62af0c4c91dfc623c7717035499 Mon Sep 17 00:00:00 2001 From: Randy Coulman Date: Tue, 14 Jul 2026 20:25:50 -0700 Subject: [PATCH 2/7] Use Req instead of HTTPoison There is some simplification/refactoring we can do, but this does the basic conversion. We also need to do some testing around how we configure the Req struct from the various options to make sure we're doing that right. --- .dialyzer_ignore.exs | 10 +---- lib/config_cat/api.ex | 47 +++++++++++++++++---- lib/config_cat/config_fetcher.ex | 10 ++--- mix.exs | 2 +- mix.lock | 20 ++++----- test/config_cat/config_fetcher_test.exs | 38 ++++++++--------- test/config_cat/data_governance_test.exs | 54 ++++++++++++------------ test/support/cache_policy_case.ex | 4 +- 8 files changed, 102 insertions(+), 83 deletions(-) diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index e9f59819..fe51488c 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,9 +1 @@ -[ - # The following four ignore rules are due to an upstream type problem in - # HTTPoison 3.0. Once https://github.com/edgurgel/httpoison/pull/511 has been - # merged and released, we should be able to delete these rules. - {"deps/httpoison/lib/httpoison/base.ex", :callback_arg_type_mismatch}, - {"deps/httpoison/lib/httpoison/base.ex", :callback_type_mismatch}, - {"deps/httpoison/lib/httpoison/base.ex", :pattern_match_cov}, - {"lib/config_cat/api.ex", :invalid_contract} -] +[] diff --git a/lib/config_cat/api.ex b/lib/config_cat/api.ex index f21c7093..f268c884 100644 --- a/lib/config_cat/api.ex +++ b/lib/config_cat/api.ex @@ -7,15 +7,46 @@ defmodule ConfigCat.API.Impl do @moduledoc false @behaviour ConfigCat.API - defdelegate get(url, headers, options), to: ConfigCat.API.HTTPoisonImpl -end + @impl ConfigCat.API + def get(url, headers, options) do + req = Req.new(decode_body: false, headers: headers, url: url) -defmodule ConfigCat.API.HTTPoisonImpl do - @moduledoc false - use HTTPoison.Base + options + |> Enum.reduce(req, &apply_option/2) + |> Req.get() + end + + defp apply_option({:proxy, nil}, req), do: req + + defp apply_option({:proxy, url}, req) do + uri = URI.parse(url) + + req + |> Req.merge( + connect_options: [ + proxy: {String.to_existing_atom(uri.scheme), uri.host, uri.port, []} + ] + ) + |> add_userinfo(uri.userinfo) + end + + defp apply_option({:recv_timeout, timeout}, req) do + Req.merge(req, receive_timeout: timeout) + end + + defp apply_option({:timeout, timeout}, req) do + Req.merge(req, connect_options: [timeout: timeout]) + end + + defp apply_option(_unused, req), do: req + + defp add_userinfo(req, nil), do: req - @impl HTTPoison.Base - def process_request_headers(headers) do - [{"Accept", "application/json"} | headers] + defp add_userinfo(req, userinfo) do + Req.merge(req, + connect_options: [ + proxy_headers: {"proxy-authorization", "Basic " <> Base.encode64(userinfo)} + ] + ) end end diff --git a/lib/config_cat/config_fetcher.ex b/lib/config_cat/config_fetcher.ex index 6e0340e0..74019ce6 100644 --- a/lib/config_cat/config_fetcher.ex +++ b/lib/config_cat/config_fetcher.ex @@ -42,7 +42,7 @@ defmodule ConfigCat.CacheControlConfigFetcher do alias ConfigCat.ConfigEntry alias ConfigCat.ConfigFetcher alias ConfigCat.ConfigFetcher.FetchError - alias HTTPoison.Response + alias Req.Response require ConfigCat.ConfigCatLogger, as: ConfigCatLogger require ConfigCat.Constants, as: Constants @@ -221,7 +221,7 @@ defmodule ConfigCat.CacheControlConfigFetcher do # This function is slightly complex, but still reasonably understandable. # Breaking it up doesn't seem like it will help much. # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity - defp handle_response(%Response{status_code: code, body: raw_config, headers: headers}, %State{} = state, etag) + defp handle_response(%Response{status: code, body: raw_config, headers: headers}, %State{} = state, etag) when code >= 200 and code < 300 do ConfigCatLogger.debug("ConfigCat configuration json fetch response code: #{code} Cached: #{extract_etag(headers)}") @@ -281,11 +281,11 @@ defmodule ConfigCat.CacheControlConfigFetcher do end end - defp handle_response(%Response{status_code: 304}, %State{} = state, _etag) do + defp handle_response(%Response{status: 304}, %State{} = state, _etag) do {:ok, :unchanged, state} end - defp handle_response(%Response{status_code: status} = response, %State{} = state, _etag) when status in [403, 404] do + defp handle_response(%Response{status: status} = response, %State{} = state, _etag) when status in [403, 404] do ConfigCatLogger.error( "Your SDK Key seems to be wrong. You can find the valid SDKKey at https://app.configcat.com/sdkkey. Received unexpected response: #{inspect(response)}", event_id: 1100 @@ -307,7 +307,7 @@ defmodule ConfigCat.CacheControlConfigFetcher do {:error, error, state} end - defp handle_error({:error, %HTTPoison.Error{reason: :checkout_timeout} = error}, %State{} = state) do + defp handle_error({:error, %Req.TransportError{reason: :timeout} = error}, %State{} = state) do ConfigCatLogger.error( "Request timed out while trying to fetch config JSON. Timeout values: [connect: #{state.connect_timeout_milliseconds}ms, read: #{state.read_timeout_milliseconds}ms]", event_id: 1102 diff --git a/mix.exs b/mix.exs index 696617f2..80e5ba51 100644 --- a/mix.exs +++ b/mix.exs @@ -82,10 +82,10 @@ defmodule ConfigCat.MixProject do {:elixir_uuid, "~> 1.2"}, {:ex_doc, "~> 0.31.0", only: :dev, runtime: false}, {:excoveralls, "~> 0.18.0", only: :test}, - {:httpoison, "~> 2.0 or ~> 3.0"}, {:jason, "~> 1.2"}, {:mix_test_interactive, "~> 5.1", only: :dev, runtime: false}, {:mox, "~> 1.1", only: :test}, + {:req, "~> 0.6.2"}, {:styler, "~> 0.11", only: [:dev, :test], runtime: false}, {:typed_struct, "~> 0.3.0"}, {:tz, "~> 0.26.5", only: :test} diff --git a/mix.lock b/mix.lock index 3d06a53b..cc990c4e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,5 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "certifi": {:hex, :certifi, "2.17.0", "835748414307e15e05b17d0e518190228ce648b08d569a5cc93a85a40f3e5c9b", [:rebar3], [], "hexpm", "8122798a17f0293c80daada25d0f81c7f4d708c73fef782c7c9b1950e26e4d21"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, @@ -9,26 +8,23 @@ "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, "excoveralls": {:hex, :excoveralls, "0.18.2", "86efd87a0676a3198ff50b8c77620ea2f445e7d414afa9ec6c4ba84c9f8bdcc2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "230262c418f0de64077626a498bd4fdf1126d5c2559bb0e6b43deac3005225a4"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "h2": {:hex, :h2, "0.10.2", "ea0146b9c8b5f3b5de16045765f5684db38ef1e66f1c60444890948cb1003e47", [:rebar3], [], "hexpm", "497a899f338b42e6a0b292524e635b0ce6f9379fa39395c8e38d06351cd9b9cf"}, - "hackney": {:hex, :hackney, "4.4.3", "644c79b2eac605724e55fca651259c0560c4eea44b4c60b64baf5a2258b99376", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.10.1", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 1.6.5", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.1", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "db6bbd96ff8562e6398c0f468ae63fc516857a0a12ecf232b3cb8e34fd167018"}, - "httpoison": {:hex, :httpoison, "3.0.0", "8566a933bb9175236d1ec335978445b67cd1f5b5d3ead6ca4b80be469d41f5d9", [:mix], [{:hackney, "~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "9130197b7658901c493d6fcfb842fb9676300fa8a6c8ed058c8889cf1a77f3c2"}, - "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, + "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, + "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.9.2", "6e89e698d69cc29be001afd02c2cf4bae2d0994efe69a2ced7aab440d71584f9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d8e952b432fdac2321f570d29a68b9eb2664dc80a7a2ef9464531a5425abf222"}, "mix_test_interactive": {:hex, :mix_test_interactive, "5.1.0", "a2edd66806f06f6a0dbb92f023126a7e4ecdf02f7d34e3bb16f76886b79bed43", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:process_tree, ">= 0.1.3", [hex: :process_tree, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "0df8ab74c176040acd78939e92a5328775bf9f414a0f0472372cce852ee548cf"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "parse_trans": {:hex, :parse_trans, "3.4.2", "c352ddc1a0d5e54f9b1654d45f9c432eef76f9cea371c55ddff769ef688fdb74", [:rebar3], [], "hexpm", "4c25347de3b7c35732d32e69ab43d1ceee0beae3f3b3ade1b59cbd3dd224d9ca"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "process_tree": {:hex, :process_tree, "0.3.0", "0eb58ec68f3d22a4f36040b0374469464c95fae5465c011b55bea01649b0bd70", [:mix], [], "hexpm", "6cb3b7be9c7d74b28a9f6e0f03115d5953e6bbda44be2b6fd9e667020870eb86"}, - "quic": {:hex, :quic, "1.6.5", "28b7d49c1732b2de3861701bb03ae7798537240552370ac49f0d139d2ea8f621", [:rebar3], [], "hexpm", "de1a88972c33201a50d1a17c8c4a14528bd1d8f25ef705897d680ae312d0aa78"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"}, "styler": {:hex, :styler, "0.11.9", "2595393b94e660cd6e8b582876337cc50ff047d184ccbed42fdad2bfd5d78af5", [:mix], [], "hexpm", "8b7806ba1fdc94d0a75127c56875f91db89b75117fcc67572661010c13e1f259"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, "tz": {:hex, :tz, "0.26.6", "4d46178dd5bc4d2c1e78c9affcc3fd46764e29cd2a148c06666edb83cb18629f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.6", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "9ca97ea48b412f2404740867f6c321ee8ce112602035bb79b0b90c9c03174652"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, - "webtransport": {:hex, :webtransport, "0.4.1", "60ef6cd99282b5964c8172d674519239e31e357d934bde028bec70da34636fe5", [:rebar3], [{:h2, "~> 0.10.1", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.6.5", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "006e4e52a8f03b69201d4637c85b424a4ddccc1909f32c5742fed8d495a75174"}, } diff --git a/test/config_cat/config_fetcher_test.exs b/test/config_cat/config_fetcher_test.exs index f33ba03b..a50a100d 100644 --- a/test/config_cat/config_fetcher_test.exs +++ b/test/config_cat/config_fetcher_test.exs @@ -9,14 +9,14 @@ defmodule ConfigCat.ConfigFetcherTest do alias ConfigCat.FetchTime alias ConfigCat.Hooks alias ConfigCat.MockAPI - alias HTTPoison.Response + alias Req.Response require ConfigCat.Constants, as: Constants setup :verify_on_exit! @config %{"key" => "value"} - @etag "ETAG" + @etag "etag" @mode "m" @raw_config Jason.encode!(@config) @sdk_key "SDK_KEY" @@ -42,7 +42,7 @@ defmodule ConfigCat.ConfigFetcherTest do url = global_config_url() stub(MockAPI, :get, fn ^url, _headers, _options -> - {:ok, %Response{status_code: 200, body: @raw_config, headers: [{"ETag", @etag}]}} + {:ok, %Response{status: 200, body: @raw_config, headers: [{"etag", @etag}]}} end) before = FetchTime.now_ms() @@ -65,7 +65,7 @@ defmodule ConfigCat.ConfigFetcherTest do expect(MockAPI, :get, 1, fn ^url, _headers, _options -> Process.sleep(50) - {:ok, %Response{status_code: 200, body: @raw_config, headers: [{"ETag", @etag}]}} + {:ok, %Response{status: 200, body: @raw_config, headers: [{"etag", @etag}]}} end) results = @@ -86,7 +86,7 @@ defmodule ConfigCat.ConfigFetcherTest do test "user agent header that includes the fetch mode" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{status_code: 200, body: @raw_config} + response = %Response{status: 200, body: @raw_config} stub(MockAPI, :get, fn _url, headers, _options -> assert_user_agent_matches(headers, ~r"^ConfigCat-Elixir/#{@mode}-") @@ -101,21 +101,21 @@ defmodule ConfigCat.ConfigFetcherTest do {:ok, fetcher} = start_fetcher(@fetcher_options) initial_response = %Response{ - status_code: 200, + status: 200, body: @raw_config, - headers: [{"ETag", @etag}] + headers: [{"etag", @etag}] } stub(MockAPI, :get, fn _url, headers, _options -> - assert List.keyfind(headers, "ETag", 0) == nil + assert List.keyfind(headers, "etag", 0) == nil {:ok, initial_response} end) {:ok, _} = ConfigFetcher.fetch(fetcher, nil) not_modified_response = %Response{ - status_code: 304, - headers: [{"ETag", @etag}] + status: 304, + headers: [{"etag", @etag}] } expect(MockAPI, :get, fn _url, headers, _options -> @@ -130,8 +130,8 @@ defmodule ConfigCat.ConfigFetcherTest do {:ok, fetcher} = start_fetcher(@fetcher_options) response = %Response{ - status_code: 304, - headers: [{"ETag", @etag}] + status: 304, + headers: [{"etag", @etag}] } stub(MockAPI, :get, fn _url, _headers, _options -> {:ok, response} end) @@ -142,7 +142,7 @@ defmodule ConfigCat.ConfigFetcherTest do {:ok, fetcher} = start_fetcher(@fetcher_options) response = %Response{ - status_code: 304, + status: 304, headers: [{"etag", @etag}] } @@ -154,7 +154,7 @@ defmodule ConfigCat.ConfigFetcherTest do test "returns error for non-200 response from ConfigCat" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{status_code: 503} + response = %Response{status: 503} stub(MockAPI, :get, fn _url, _headers, _options -> {:ok, response} end) assert {:error, %FetchError{reason: ^response}} = ConfigFetcher.fetch(fetcher, nil) @@ -164,7 +164,7 @@ defmodule ConfigCat.ConfigFetcherTest do test "returns error for error response from ConfigCat" do {:ok, fetcher} = start_fetcher(@fetcher_options) - error = %HTTPoison.Error{reason: "failed"} + error = %Req.TransportError{reason: "failed"} stub(MockAPI, :get, fn _url, _headers, _options -> {:error, error} end) assert {:error, %FetchError{reason: ^error}} = ConfigFetcher.fetch(fetcher, nil) @@ -177,14 +177,14 @@ defmodule ConfigCat.ConfigFetcherTest do url = config_url(base_url, @sdk_key) - expect(MockAPI, :get, fn ^url, _headers, _options -> {:ok, %Response{status_code: 200, body: @raw_config}} end) + expect(MockAPI, :get, fn ^url, _headers, _options -> {:ok, %Response{status: 200, body: @raw_config}} end) {:ok, _} = ConfigFetcher.fetch(fetcher, nil) end test "uses default timeouts if none provided" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{status_code: 200, body: @raw_config} + response = %Response{status: 200, body: @raw_config} expect(MockAPI, :get, fn _url, _headers, options -> assert Keyword.get(options, :recv_timeout) == 5000 @@ -205,7 +205,7 @@ defmodule ConfigCat.ConfigFetcherTest do read_timeout_milliseconds: read_timeout ) - response = %Response{status_code: 200, body: @raw_config} + response = %Response{status: 200, body: @raw_config} expect(MockAPI, :get, fn _url, _headers, options -> assert Keyword.get(options, :recv_timeout) == read_timeout @@ -220,7 +220,7 @@ defmodule ConfigCat.ConfigFetcherTest do proxy = "https://PROXY" {:ok, fetcher} = start_fetcher(@fetcher_options, http_proxy: proxy) - response = %Response{status_code: 200, body: @raw_config} + response = %Response{status: 200, body: @raw_config} expect(MockAPI, :get, fn _url, _headers, options -> assert Keyword.get(options, :proxy) == proxy diff --git a/test/config_cat/data_governance_test.exs b/test/config_cat/data_governance_test.exs index 8e7372d9..7d86a386 100644 --- a/test/config_cat/data_governance_test.exs +++ b/test/config_cat/data_governance_test.exs @@ -9,7 +9,7 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do alias ConfigCat.ConfigEntry alias ConfigCat.Hooks alias ConfigCat.MockAPI - alias HTTPoison.Response + alias Req.Response require ConfigCat.Constants, as: Constants require ConfigCat.RedirectMode, as: RedirectMode @@ -50,13 +50,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 2, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: raw_config}} + {:ok, %Response{status: 200, body: raw_config}} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: raw_config}} + {:ok, %Response{status: 200, body: raw_config}} end) |> expect(:get, 0, fn ^redirect_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: raw_config}} + {:ok, %Response{status: 200, body: raw_config}} end) assert {:ok, %ConfigEntry{config: ^config}} = ConfigFetcher.fetch(fetcher, nil) @@ -75,13 +75,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: raw_config}} + {:ok, %Response{status: 200, body: raw_config}} end) |> expect(:get, 2, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: raw_config}} + {:ok, %Response{status: 200, body: raw_config}} end) |> expect(:get, 0, fn ^redirect_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: raw_config}} + {:ok, %Response{status: 200, body: raw_config}} end) assert {:ok, %ConfigEntry{config: ^config}} = ConfigFetcher.fetch(fetcher, nil) @@ -99,10 +99,10 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_eu}} + {:ok, %Response{status: 200, body: config_to_eu}} end) |> expect(:get, 2, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_eu}} + {:ok, %Response{status: 200, body: config_eu}} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -120,10 +120,10 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_eu}} + {:ok, %Response{status: 200, body: config_to_eu}} end) |> expect(:get, 2, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_eu}} + {:ok, %Response{status: 200, body: config_eu}} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -145,13 +145,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 2, fn ^custom_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_global}} + {:ok, %Response{status: 200, body: config_to_global}} end) |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: %{}}} + {:ok, %Response{status: 200, body: %{}}} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: %{}}} + {:ok, %Response{status: 200, body: %{}}} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -173,13 +173,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 2, fn ^custom_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_eu}} + {:ok, %Response{status: 200, body: config_to_eu}} end) |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: %{}}} + {:ok, %Response{status: 200, body: %{}}} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: %{}}} + {:ok, %Response{status: 200, body: %{}}} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -197,10 +197,10 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_forced}} + {:ok, %Response{status: 200, body: config_to_forced}} end) |> expect(:get, 2, fn ^forced_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: "{}"}} + {:ok, %Response{status: 200, body: "{}"}} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> :not_called @@ -226,16 +226,16 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: %{}}} + {:ok, %Response{status: 200, body: %{}}} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: %{}}} + {:ok, %Response{status: 200, body: %{}}} end) |> expect(:get, 1, fn ^custom_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_forced}} + {:ok, %Response{status: 200, body: config_to_forced}} end) |> expect(:get, 3, fn ^forced_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_forced}} + {:ok, %Response{status: 200, body: config_to_forced}} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -253,20 +253,20 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_eu}} + {:ok, %Response{status: 200, body: config_to_eu}} end) |> expect(:get, 1, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_global}} + {:ok, %Response{status: 200, body: config_to_global}} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) MockAPI |> expect(:get, 1, fn ^eu_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_global}} + {:ok, %Response{status: 200, body: config_to_global}} end) |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status_code: 200, body: config_to_eu}} + {:ok, %Response{status: 200, body: config_to_eu}} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) diff --git a/test/support/cache_policy_case.ex b/test/support/cache_policy_case.ex index b2345f4b..5a683e13 100644 --- a/test/support/cache_policy_case.ex +++ b/test/support/cache_policy_case.ex @@ -14,7 +14,7 @@ defmodule ConfigCat.CachePolicyCase do alias ConfigCat.Hooks alias ConfigCat.InMemoryCache alias ConfigCat.MockFetcher - alias HTTPoison.Response + alias Req.Response using do quote do @@ -112,7 +112,7 @@ defmodule ConfigCat.CachePolicyCase do @spec assert_returns_error(function()) :: true def assert_returns_error(force_refresh_fn) do - response = %Response{status_code: 503} + response = %Response{status: 503} error = FetchError.exception(reason: response, transient?: true) stub(MockFetcher, :fetch, fn _id, _etag -> {:error, error} end) From 0a73c957e56ef1c39cbc42525f0124ad2584feb9 Mon Sep 17 00:00:00 2001 From: Randy Coulman Date: Tue, 14 Jul 2026 20:43:54 -0700 Subject: [PATCH 3/7] More reliable etag header extraction Use Req's `get_header` function to extract the etag. This caused some tests to fail due to Req's different internal representation of headers, so switch all response creation to use `Response.new` and `Response.put_header`. --- lib/config_cat/config_fetcher.ex | 14 +++--- test/config_cat/config_fetcher_test.exs | 60 ++++++++++++++---------- test/config_cat/data_governance_test.exs | 52 ++++++++++---------- test/support/cache_policy_case.ex | 2 +- 4 files changed, 69 insertions(+), 59 deletions(-) diff --git a/lib/config_cat/config_fetcher.ex b/lib/config_cat/config_fetcher.ex index 74019ce6..a20903f7 100644 --- a/lib/config_cat/config_fetcher.ex +++ b/lib/config_cat/config_fetcher.ex @@ -221,13 +221,13 @@ defmodule ConfigCat.CacheControlConfigFetcher do # This function is slightly complex, but still reasonably understandable. # Breaking it up doesn't seem like it will help much. # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity - defp handle_response(%Response{status: code, body: raw_config, headers: headers}, %State{} = state, etag) + defp handle_response(%Response{status: code, body: raw_config} = response, %State{} = state, etag) when code >= 200 and code < 300 do - ConfigCatLogger.debug("ConfigCat configuration json fetch response code: #{code} Cached: #{extract_etag(headers)}") + ConfigCatLogger.debug("ConfigCat configuration json fetch response code: #{code} Cached: #{extract_etag(response)}") with {:ok, decoded_config} <- Jason.decode(raw_config), config = Config.inline_salt_and_segments(decoded_config), - new_etag = extract_etag(headers), + new_etag = extract_etag(response), %{base_url: new_base_url, custom_endpoint?: custom_endpoint?, redirects: redirects} <- state do preferences = Config.preferences(config) @@ -328,10 +328,10 @@ defmodule ConfigCat.CacheControlConfigFetcher do FetchError.exception(reason: error, transient?: true) end - defp extract_etag(headers) do - case Enum.find(headers, fn {key, _value} -> String.downcase(key) == "etag" end) do - nil -> nil - {_key, value} -> value + defp extract_etag(%Response{} = response) do + case Response.get_header(response, "etag") do + [] -> nil + [etag] -> etag end end end diff --git a/test/config_cat/config_fetcher_test.exs b/test/config_cat/config_fetcher_test.exs index a50a100d..83e41631 100644 --- a/test/config_cat/config_fetcher_test.exs +++ b/test/config_cat/config_fetcher_test.exs @@ -42,7 +42,12 @@ defmodule ConfigCat.ConfigFetcherTest do url = global_config_url() stub(MockAPI, :get, fn ^url, _headers, _options -> - {:ok, %Response{status: 200, body: @raw_config, headers: [{"etag", @etag}]}} + response = + [body: @raw_config, status: 200] + |> Response.new() + |> Response.put_header("etag", @etag) + + {:ok, response} end) before = FetchTime.now_ms() @@ -65,7 +70,13 @@ defmodule ConfigCat.ConfigFetcherTest do expect(MockAPI, :get, 1, fn ^url, _headers, _options -> Process.sleep(50) - {:ok, %Response{status: 200, body: @raw_config, headers: [{"etag", @etag}]}} + + response = + [body: @raw_config, status: 200] + |> Response.new() + |> Response.put_header("etag", @etag) + + {:ok, response} end) results = @@ -86,7 +97,7 @@ defmodule ConfigCat.ConfigFetcherTest do test "user agent header that includes the fetch mode" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{status: 200, body: @raw_config} + response = Response.new(body: @raw_config, status: 200) stub(MockAPI, :get, fn _url, headers, _options -> assert_user_agent_matches(headers, ~r"^ConfigCat-Elixir/#{@mode}-") @@ -100,11 +111,10 @@ defmodule ConfigCat.ConfigFetcherTest do test "sends proper cache control header on later requests" do {:ok, fetcher} = start_fetcher(@fetcher_options) - initial_response = %Response{ - status: 200, - body: @raw_config, - headers: [{"etag", @etag}] - } + initial_response = + [body: @raw_config, status: 200] + |> Response.new() + |> Response.put_header("etag", @etag) stub(MockAPI, :get, fn _url, headers, _options -> assert List.keyfind(headers, "etag", 0) == nil @@ -113,10 +123,10 @@ defmodule ConfigCat.ConfigFetcherTest do {:ok, _} = ConfigFetcher.fetch(fetcher, nil) - not_modified_response = %Response{ - status: 304, - headers: [{"etag", @etag}] - } + not_modified_response = + [status: 304] + |> Response.new() + |> Response.put_header("etag", @etag) expect(MockAPI, :get, fn _url, headers, _options -> assert {"If-None-Match", @etag} = List.keyfind(headers, "If-None-Match", 0) @@ -129,10 +139,10 @@ defmodule ConfigCat.ConfigFetcherTest do test "returns unchanged response when server responds that the config hasn't changed" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{ - status: 304, - headers: [{"etag", @etag}] - } + response = + [status: 304] + |> Response.new() + |> Response.put_header("etag", @etag) stub(MockAPI, :get, fn _url, _headers, _options -> {:ok, response} end) assert {:ok, :unchanged} = ConfigFetcher.fetch(fetcher, @etag) @@ -141,10 +151,10 @@ defmodule ConfigCat.ConfigFetcherTest do test "returns unchanged response (with lowercase 'etag') when server responds that the config hasn't changed" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{ - status: 304, - headers: [{"etag", @etag}] - } + response = + [status: 304] + |> Response.new() + |> Response.put_header("etag", @etag) stub(MockAPI, :get, fn _url, _headers, _options -> {:ok, response} end) assert {:ok, :unchanged} = ConfigFetcher.fetch(fetcher, @etag) @@ -154,7 +164,7 @@ defmodule ConfigCat.ConfigFetcherTest do test "returns error for non-200 response from ConfigCat" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{status: 503} + response = Response.new(status: 503) stub(MockAPI, :get, fn _url, _headers, _options -> {:ok, response} end) assert {:error, %FetchError{reason: ^response}} = ConfigFetcher.fetch(fetcher, nil) @@ -177,14 +187,14 @@ defmodule ConfigCat.ConfigFetcherTest do url = config_url(base_url, @sdk_key) - expect(MockAPI, :get, fn ^url, _headers, _options -> {:ok, %Response{status: 200, body: @raw_config}} end) + expect(MockAPI, :get, fn ^url, _headers, _options -> {:ok, Response.new(body: @raw_config, status: 200)} end) {:ok, _} = ConfigFetcher.fetch(fetcher, nil) end test "uses default timeouts if none provided" do {:ok, fetcher} = start_fetcher(@fetcher_options) - response = %Response{status: 200, body: @raw_config} + response = Response.new(body: @raw_config, status: 200) expect(MockAPI, :get, fn _url, _headers, options -> assert Keyword.get(options, :recv_timeout) == 5000 @@ -205,7 +215,7 @@ defmodule ConfigCat.ConfigFetcherTest do read_timeout_milliseconds: read_timeout ) - response = %Response{status: 200, body: @raw_config} + response = Response.new(body: @raw_config, status: 200) expect(MockAPI, :get, fn _url, _headers, options -> assert Keyword.get(options, :recv_timeout) == read_timeout @@ -220,7 +230,7 @@ defmodule ConfigCat.ConfigFetcherTest do proxy = "https://PROXY" {:ok, fetcher} = start_fetcher(@fetcher_options, http_proxy: proxy) - response = %Response{status: 200, body: @raw_config} + response = Response.new(body: @raw_config, status: 200) expect(MockAPI, :get, fn _url, _headers, options -> assert Keyword.get(options, :proxy) == proxy diff --git a/test/config_cat/data_governance_test.exs b/test/config_cat/data_governance_test.exs index 7d86a386..45457397 100644 --- a/test/config_cat/data_governance_test.exs +++ b/test/config_cat/data_governance_test.exs @@ -50,13 +50,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 2, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: raw_config}} + {:ok, Response.new(body: raw_config, status: 200)} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: raw_config}} + {:ok, Response.new(body: raw_config, status: 200)} end) |> expect(:get, 0, fn ^redirect_url, _headers, _options -> - {:ok, %Response{status: 200, body: raw_config}} + {:ok, Response.new(body: raw_config, status: 200)} end) assert {:ok, %ConfigEntry{config: ^config}} = ConfigFetcher.fetch(fetcher, nil) @@ -75,13 +75,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: raw_config}} + {:ok, Response.new(body: raw_config, status: 200)} end) |> expect(:get, 2, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: raw_config}} + {:ok, Response.new(body: raw_config, status: 200)} end) |> expect(:get, 0, fn ^redirect_url, _headers, _options -> - {:ok, %Response{status: 200, body: raw_config}} + {:ok, Response.new(body: raw_config, status: 200)} end) assert {:ok, %ConfigEntry{config: ^config}} = ConfigFetcher.fetch(fetcher, nil) @@ -99,10 +99,10 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_eu}} + {:ok, Response.new(body: config_to_eu, status: 200)} end) |> expect(:get, 2, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_eu}} + {:ok, Response.new(body: config_eu, status: 200)} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -120,10 +120,10 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_eu}} + {:ok, Response.new(body: config_to_eu, status: 200)} end) |> expect(:get, 2, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_eu}} + {:ok, Response.new(body: config_eu, status: 200)} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -145,13 +145,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 2, fn ^custom_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_global}} + {:ok, Response.new(body: config_to_global, status: 200)} end) |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: %{}}} + {:ok, Response.new(body: %{}, status: 200)} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: %{}}} + {:ok, Response.new(body: %{}, status: 200)} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -173,13 +173,13 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 2, fn ^custom_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_eu}} + {:ok, Response.new(body: config_to_eu, status: 200)} end) |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: %{}}} + {:ok, Response.new(body: %{}, status: 200)} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: %{}}} + {:ok, Response.new(body: %{}, status: 200)} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -197,10 +197,10 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_forced}} + {:ok, Response.new(body: config_to_forced, status: 200)} end) |> expect(:get, 2, fn ^forced_url, _headers, _options -> - {:ok, %Response{status: 200, body: "{}"}} + {:ok, Response.new(body: "{}", status: 200)} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> :not_called @@ -226,16 +226,16 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 0, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: %{}}} + {:ok, Response.new(body: %{}, status: 200)} end) |> expect(:get, 0, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: %{}}} + {:ok, Response.new(body: %{}, status: 200)} end) |> expect(:get, 1, fn ^custom_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_forced}} + {:ok, Response.new(body: config_to_forced, status: 200)} end) |> expect(:get, 3, fn ^forced_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_forced}} + {:ok, Response.new(body: config_to_forced, status: 200)} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) @@ -253,20 +253,20 @@ defmodule ConfigCat.ConfigFetcher.DataGovernanceTest do MockAPI |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_eu}} + {:ok, Response.new(body: config_to_eu, status: 200)} end) |> expect(:get, 1, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_global}} + {:ok, Response.new(body: config_to_global, status: 200)} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) MockAPI |> expect(:get, 1, fn ^eu_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_global}} + {:ok, Response.new(body: config_to_global, status: 200)} end) |> expect(:get, 1, fn ^global_url, _headers, _options -> - {:ok, %Response{status: 200, body: config_to_eu}} + {:ok, Response.new(body: config_to_eu, status: 200)} end) assert {:ok, _} = ConfigFetcher.fetch(fetcher, nil) diff --git a/test/support/cache_policy_case.ex b/test/support/cache_policy_case.ex index 5a683e13..e971d8e2 100644 --- a/test/support/cache_policy_case.ex +++ b/test/support/cache_policy_case.ex @@ -112,7 +112,7 @@ defmodule ConfigCat.CachePolicyCase do @spec assert_returns_error(function()) :: true def assert_returns_error(force_refresh_fn) do - response = %Response{status: 503} + response = Response.new(status: 503) error = FetchError.exception(reason: response, transient?: true) stub(MockFetcher, :fetch, fn _id, _etag -> {:error, error} end) From 949f86e19726320cca6f693bbb8ad35010ef8c1c Mon Sep 17 00:00:00 2001 From: Randy Coulman Date: Tue, 14 Jul 2026 21:40:22 -0700 Subject: [PATCH 4/7] Add tests for API option handling Found and fixed bugs in the initial implementation. --- lib/config_cat/api.ex | 42 ++++++++++------ lib/config_cat/config_fetcher.ex | 2 +- test/config_cat/api_test.exs | 84 ++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 test/config_cat/api_test.exs diff --git a/lib/config_cat/api.ex b/lib/config_cat/api.ex index f268c884..4ce0e75d 100644 --- a/lib/config_cat/api.ex +++ b/lib/config_cat/api.ex @@ -3,39 +3,43 @@ defmodule ConfigCat.API do @callback get(binary(), [{binary(), binary()}], Keyword.t()) :: {:ok, struct()} | {:error, struct()} end -defmodule ConfigCat.API.Impl do +defmodule ConfigCat.API.ReqAPI do @moduledoc false @behaviour ConfigCat.API + alias Req.Request + @impl ConfigCat.API def get(url, headers, options) do + req = build_request(url, headers, options) + Req.get(req) + end + + @doc false + @spec build_request(binary(), [{binary(), binary()}], Keyword.t()) :: Req.Request.t() + def build_request(url, headers, options) do req = Req.new(decode_body: false, headers: headers, url: url) - options - |> Enum.reduce(req, &apply_option/2) - |> Req.get() + Enum.reduce(options, req, &apply_option/2) end defp apply_option({:proxy, nil}, req), do: req defp apply_option({:proxy, url}, req) do uri = URI.parse(url) + proxy = {String.to_existing_atom(uri.scheme), uri.host, uri.port, []} req - |> Req.merge( - connect_options: [ - proxy: {String.to_existing_atom(uri.scheme), uri.host, uri.port, []} - ] - ) + |> merge_connect_option(:proxy, proxy) |> add_userinfo(uri.userinfo) end defp apply_option({:recv_timeout, timeout}, req) do - Req.merge(req, receive_timeout: timeout) + Request.put_option(req, :receive_timeout, timeout) end defp apply_option({:timeout, timeout}, req) do - Req.merge(req, connect_options: [timeout: timeout]) + merge_connect_option(req, :timeout, timeout) end defp apply_option(_unused, req), do: req @@ -43,10 +47,16 @@ defmodule ConfigCat.API.Impl do defp add_userinfo(req, nil), do: req defp add_userinfo(req, userinfo) do - Req.merge(req, - connect_options: [ - proxy_headers: {"proxy-authorization", "Basic " <> Base.encode64(userinfo)} - ] - ) + proxy_headers = {"proxy-authorization", "Basic " <> Base.encode64(userinfo)} + merge_connect_option(req, :proxy_headers, proxy_headers) + end + + defp merge_connect_option(req, key, value) do + new_connect_options = + req + |> Request.get_option(:connect_options, []) + |> Keyword.put(key, value) + + Request.put_option(req, :connect_options, new_connect_options) end end diff --git a/lib/config_cat/config_fetcher.ex b/lib/config_cat/config_fetcher.ex index a20903f7..389b8374 100644 --- a/lib/config_cat/config_fetcher.ex +++ b/lib/config_cat/config_fetcher.ex @@ -53,7 +53,7 @@ defmodule ConfigCat.CacheControlConfigFetcher do use TypedStruct typedstruct enforce: true do - field :api, module(), default: ConfigCat.API.Impl + field :api, module(), default: ConfigCat.API.ReqAPI field :base_url, String.t() field :callers, [GenServer.from()], default: [] field :connect_timeout_milliseconds, non_neg_integer(), default: 8_000 diff --git a/test/config_cat/api_test.exs b/test/config_cat/api_test.exs new file mode 100644 index 00000000..d63a683a --- /dev/null +++ b/test/config_cat/api_test.exs @@ -0,0 +1,84 @@ +defmodule ConfigCat.API.ReqAPITest do + use ExUnit.Case, async: false + + alias ConfigCat.API.ReqAPI + alias Req.Request + + @url "https://some.example.com" + + describe "request building" do + test "includes URL" do + request = ReqAPI.build_request(@url, [], []) + + assert URI.to_string(request.url) == @url + end + + test "does not automatically decode the response body" do + request = ReqAPI.build_request(@url, [], []) + assert Request.get_option(request, :decode_body) == false + end + + test "lowercases and includes provided headers" do + user_agent = "Some-User-Agent" + headers = [{"User-Agent", user_agent}] + request = ReqAPI.build_request(@url, headers, []) + + assert Request.get_header(request, "user-agent") == [user_agent] + end + + test "handles read timeout option" do + timeout = 10 + request = ReqAPI.build_request(@url, [], recv_timeout: timeout) + + assert Request.get_option(request, :receive_timeout) == timeout + end + + test "handles connection timeout option" do + timeout = 15 + request = ReqAPI.build_request(@url, [], timeout: timeout) + + connect_options = Request.get_option(request, :connect_options, []) + assert connect_options[:timeout] == timeout + end + + test "handles simple proxy" do + proxy_url = "https://example.com" + request = ReqAPI.build_request(@url, [], proxy: proxy_url) + + connect_options = Request.get_option(request, :connect_options, []) + assert connect_options[:proxy] == {:https, "example.com", 443, []} + end + + test "handles proxy with custom port" do + proxy_url = "https://example.com:1234" + request = ReqAPI.build_request(@url, [], proxy: proxy_url) + + connect_options = Request.get_option(request, :connect_options, []) + assert connect_options[:proxy] == {:https, "example.com", 1234, []} + end + + test "handles proxy with username/password" do + proxy_url = "https://user:pass@example.com" + request = ReqAPI.build_request(@url, [], proxy: proxy_url) + + connect_options = Request.get_option(request, :connect_options, []) + assert connect_options[:proxy] == {:https, "example.com", 443, []} + assert {"proxy-authorization", "Basic " <> encoded_userinfo} = connect_options[:proxy_headers] + assert Base.decode64!(encoded_userinfo) == "user:pass" + end + + test "properly merges options" do + receive_timeout = 8 + timeout = 12 + proxy_url = "https://user:pass@example.com:1234" + request = ReqAPI.build_request(@url, [], proxy: proxy_url, recv_timeout: receive_timeout, timeout: timeout) + + connect_options = Request.get_option(request, :connect_options, []) + assert Request.get_option(request, :receive_timeout) == receive_timeout + assert connect_options[:timeout] == timeout + assert connect_options[:proxy] == {:https, "example.com", 1234, []} + assert {"proxy-authorization", "Basic " <> encoded_userinfo} = connect_options[:proxy_headers] + assert Base.decode64!(encoded_userinfo) == "user:pass" + end + end +end From 9d03d56b200cad9fd57f2d64ce2fa7afc157a981 Mon Sep 17 00:00:00 2001 From: Randy Coulman Date: Tue, 14 Jul 2026 21:52:51 -0700 Subject: [PATCH 5/7] Refactor: use original option names in API Rather than mapping option names to the old HTTPoison/Hackney names in ConfigFetcher, pass the options as-is to the API and let it figure out what to do with them. --- lib/config_cat/api.ex | 24 ++++++++++++++---------- lib/config_cat/config_fetcher.ex | 11 +++-------- test/config_cat/api_test.exs | 18 ++++++++++++------ test/config_cat/config_fetcher_test.exs | 10 +++++----- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/lib/config_cat/api.ex b/lib/config_cat/api.ex index 4ce0e75d..47af0913 100644 --- a/lib/config_cat/api.ex +++ b/lib/config_cat/api.ex @@ -1,6 +1,11 @@ defmodule ConfigCat.API do @moduledoc false - @callback get(binary(), [{binary(), binary()}], Keyword.t()) :: {:ok, struct()} | {:error, struct()} + @type option :: + {:connect_timeout_milliseconds, non_neg_integer()} + | {:http_proxy, String.t() | nil} + | {:read_timeout_milliseconds, non_neg_integer()} + + @callback get(binary(), [{binary(), binary()}], [option]) :: {:ok, Req.Request.t()} | {:error, Exception.t()} end defmodule ConfigCat.API.ReqAPI do @@ -18,14 +23,19 @@ defmodule ConfigCat.API.ReqAPI do @doc false @spec build_request(binary(), [{binary(), binary()}], Keyword.t()) :: Req.Request.t() def build_request(url, headers, options) do + options = Keyword.validate!(options, [:connect_timeout_milliseconds, :http_proxy, :read_timeout_milliseconds]) req = Req.new(decode_body: false, headers: headers, url: url) Enum.reduce(options, req, &apply_option/2) end - defp apply_option({:proxy, nil}, req), do: req + defp apply_option({:connect_timeout_milliseconds, timeout}, req) do + merge_connect_option(req, :timeout, timeout) + end + + defp apply_option({:http_proxy, nil}, req), do: req - defp apply_option({:proxy, url}, req) do + defp apply_option({:http_proxy, url}, req) do uri = URI.parse(url) proxy = {String.to_existing_atom(uri.scheme), uri.host, uri.port, []} @@ -34,16 +44,10 @@ defmodule ConfigCat.API.ReqAPI do |> add_userinfo(uri.userinfo) end - defp apply_option({:recv_timeout, timeout}, req) do + defp apply_option({:read_timeout_milliseconds, timeout}, req) do Request.put_option(req, :receive_timeout, timeout) end - defp apply_option({:timeout, timeout}, req) do - merge_connect_option(req, :timeout, timeout) - end - - defp apply_option(_unused, req), do: req - defp add_userinfo(req, nil), do: req defp add_userinfo(req, userinfo) do diff --git a/lib/config_cat/config_fetcher.ex b/lib/config_cat/config_fetcher.ex index 389b8374..83fcb2af 100644 --- a/lib/config_cat/config_fetcher.ex +++ b/lib/config_cat/config_fetcher.ex @@ -208,14 +208,9 @@ defmodule ConfigCat.CacheControlConfigFetcher do end defp http_options(%State{} = state) do - options = - Map.take(state, [:http_proxy, :connect_timeout_milliseconds, :read_timeout_milliseconds]) - - Enum.map(options, fn - {:http_proxy, value} -> {:proxy, value} - {:connect_timeout_milliseconds, value} -> {:timeout, value} - {:read_timeout_milliseconds, value} -> {:recv_timeout, value} - end) + state + |> Map.take([:connect_timeout_milliseconds, :http_proxy, :read_timeout_milliseconds]) + |> Keyword.new() end # This function is slightly complex, but still reasonably understandable. diff --git a/test/config_cat/api_test.exs b/test/config_cat/api_test.exs index d63a683a..5fa9acd4 100644 --- a/test/config_cat/api_test.exs +++ b/test/config_cat/api_test.exs @@ -28,14 +28,14 @@ defmodule ConfigCat.API.ReqAPITest do test "handles read timeout option" do timeout = 10 - request = ReqAPI.build_request(@url, [], recv_timeout: timeout) + request = ReqAPI.build_request(@url, [], read_timeout_milliseconds: timeout) assert Request.get_option(request, :receive_timeout) == timeout end test "handles connection timeout option" do timeout = 15 - request = ReqAPI.build_request(@url, [], timeout: timeout) + request = ReqAPI.build_request(@url, [], connect_timeout_milliseconds: timeout) connect_options = Request.get_option(request, :connect_options, []) assert connect_options[:timeout] == timeout @@ -43,7 +43,7 @@ defmodule ConfigCat.API.ReqAPITest do test "handles simple proxy" do proxy_url = "https://example.com" - request = ReqAPI.build_request(@url, [], proxy: proxy_url) + request = ReqAPI.build_request(@url, [], http_proxy: proxy_url) connect_options = Request.get_option(request, :connect_options, []) assert connect_options[:proxy] == {:https, "example.com", 443, []} @@ -51,7 +51,7 @@ defmodule ConfigCat.API.ReqAPITest do test "handles proxy with custom port" do proxy_url = "https://example.com:1234" - request = ReqAPI.build_request(@url, [], proxy: proxy_url) + request = ReqAPI.build_request(@url, [], http_proxy: proxy_url) connect_options = Request.get_option(request, :connect_options, []) assert connect_options[:proxy] == {:https, "example.com", 1234, []} @@ -59,7 +59,7 @@ defmodule ConfigCat.API.ReqAPITest do test "handles proxy with username/password" do proxy_url = "https://user:pass@example.com" - request = ReqAPI.build_request(@url, [], proxy: proxy_url) + request = ReqAPI.build_request(@url, [], http_proxy: proxy_url) connect_options = Request.get_option(request, :connect_options, []) assert connect_options[:proxy] == {:https, "example.com", 443, []} @@ -71,7 +71,13 @@ defmodule ConfigCat.API.ReqAPITest do receive_timeout = 8 timeout = 12 proxy_url = "https://user:pass@example.com:1234" - request = ReqAPI.build_request(@url, [], proxy: proxy_url, recv_timeout: receive_timeout, timeout: timeout) + + request = + ReqAPI.build_request(@url, [], + connect_timeout_milliseconds: timeout, + http_proxy: proxy_url, + read_timeout_milliseconds: receive_timeout + ) connect_options = Request.get_option(request, :connect_options, []) assert Request.get_option(request, :receive_timeout) == receive_timeout diff --git a/test/config_cat/config_fetcher_test.exs b/test/config_cat/config_fetcher_test.exs index 83e41631..c56f5e77 100644 --- a/test/config_cat/config_fetcher_test.exs +++ b/test/config_cat/config_fetcher_test.exs @@ -197,8 +197,8 @@ defmodule ConfigCat.ConfigFetcherTest do response = Response.new(body: @raw_config, status: 200) expect(MockAPI, :get, fn _url, _headers, options -> - assert Keyword.get(options, :recv_timeout) == 5000 - assert Keyword.get(options, :timeout) == 8000 + assert Keyword.get(options, :connect_timeout_milliseconds) == 8000 + assert Keyword.get(options, :read_timeout_milliseconds) == 5000 {:ok, response} end) @@ -218,8 +218,8 @@ defmodule ConfigCat.ConfigFetcherTest do response = Response.new(body: @raw_config, status: 200) expect(MockAPI, :get, fn _url, _headers, options -> - assert Keyword.get(options, :recv_timeout) == read_timeout - assert Keyword.get(options, :timeout) == connect_timeout + assert Keyword.get(options, :connect_timeout_milliseconds) == connect_timeout + assert Keyword.get(options, :read_timeout_milliseconds) == read_timeout {:ok, response} end) @@ -233,7 +233,7 @@ defmodule ConfigCat.ConfigFetcherTest do response = Response.new(body: @raw_config, status: 200) expect(MockAPI, :get, fn _url, _headers, options -> - assert Keyword.get(options, :proxy) == proxy + assert Keyword.get(options, :http_proxy) == proxy {:ok, response} end) From 1ce598af68790125bcc7a722715e2d5ab01fdbf3 Mon Sep 17 00:00:00 2001 From: kp-cat <52385411+kp-cat@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:21:09 +0200 Subject: [PATCH 6/7] skippable proxy test --- test/integration_test.exs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/integration_test.exs b/test/integration_test.exs index a7124fd7..465ef554 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -165,6 +165,36 @@ defmodule ConfigCat.IntegrationTest do "default value" end + @tag capture_log: true + @tag skip: + unless( + System.get_env("CONFIGCAT_RUN_PROXY_TEST") in ["1", "true", "TRUE"], + do: "set CONFIGCAT_RUN_PROXY_TEST=true to enable proxy integration test" + ) + test "fetches config through proxy" do + # Run a local Squid proxy for this test: + # docker run -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta + # + # Verify the proxy: + # curl --proxy localhost:3128 https://cdn-global.configcat.com/configuration-files/PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A/config_v6.json + # + # Run the test: + # CONFIGCAT_RUN_PROXY_TEST=true mix test --no-deps-check test/integration_test.exs + proxy_sdk_key = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A" + proxy_url = System.get_env("CONFIGCAT_PROXY_URL") || "http://localhost:3128" + + {:ok, client} = + start( + proxy_sdk_key, + cache_policy: CachePolicy.manual(), + http_proxy: proxy_url + ) + + :ok = ConfigCat.force_refresh(client: client) + + assert ConfigCat.get_value("stringDefaultCat", "", client: client) == "Cat" + end + defp start(sdk_key, options \\ []) do sdk_key |> Cache.generate_key() From 355265129bd4334c9e1523107223e3ff99c81467 Mon Sep 17 00:00:00 2001 From: Randy Coulman Date: Fri, 17 Jul 2026 10:02:17 -0700 Subject: [PATCH 7/7] Update sample lockfiles --- samples/multi/mix.lock | 17 ++++++++--------- samples/simple/mix.lock | 17 ++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/samples/multi/mix.lock b/samples/multi/mix.lock index 5c64cf5a..dd24a827 100644 --- a/samples/multi/mix.lock +++ b/samples/multi/mix.lock @@ -1,14 +1,13 @@ %{ - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, + "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.9.2", "6e89e698d69cc29be001afd02c2cf4bae2d0994efe69a2ced7aab440d71584f9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d8e952b432fdac2321f570d29a68b9eb2664dc80a7a2ef9464531a5425abf222"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/samples/simple/mix.lock b/samples/simple/mix.lock index 5c64cf5a..dd24a827 100644 --- a/samples/simple/mix.lock +++ b/samples/simple/mix.lock @@ -1,14 +1,13 @@ %{ - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, + "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.9.2", "6e89e698d69cc29be001afd02c2cf4bae2d0994efe69a2ced7aab440d71584f9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d8e952b432fdac2321f570d29a68b9eb2664dc80a7a2ef9464531a5425abf222"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, }