Skip to content

Pluggable HTTP client#149

Open
up2jj wants to merge 7 commits into
configcat:mainfrom
up2jj:pluggable-http-client
Open

Pluggable HTTP client#149
up2jj wants to merge 7 commits into
configcat:mainfrom
up2jj:pluggable-http-client

Conversation

@up2jj
Copy link
Copy Markdown

@up2jj up2jj commented May 13, 2026

Describe the purpose of your pull request

Make the HTTP transport pluggable so the SDK no longer hard-codes HTTPoison. Apps can keep the current default or supply their own client (Req, Mint, Finch, Tesla, a test stub, ...) via a small behaviour.

  • New ConfigCat.HTTPClient behaviour with a normalized request/response contract (plain maps — no HTTP-client structs leak across the boundary). Adapters are responsible for classifying errors as transient vs. permanent.
  • ConfigCat.HTTPClient.HTTPoison — the default adapter, wraps HTTPoison. Guards itself with Code.ensure_loaded?/1 and raises ArgumentError if HTTPoison is missing and no custom client was provided. (Replaces the previous internal ConfigCat.API.)
  • :httpoison is now declared optional: true in mix.exs. Consumer apps add it explicitly — or skip it entirely if they ship their own adapter.
  • Adapters match on map shape instead of struct patterns, so the SDK compiles cleanly when the optional dep is absent.
  • ConfigFetcher drops all HTTPoison struct pattern matches and consumes the normalized map contract instead. The internal :api field is renamed to :http_client; the supervisor plumbs a new :http_client start option through to the fetcher.
  • README gets an HTTP Client section covering the default HTTPoison path and the custom adapter path; the ConfigCat moduledoc documents the new option.

Backwards compatible on the wire: the default adapter preserves today's headers, options pass-through, and transient/permanent error split.

Related issues (only if applicable)

N/A

How to test?

  • mix test — the full suite (436 tests) passes against the new default adapter; fetcher and data-governance tests were updated to use normalized response maps instead of %HTTPoison.Response{} and the http_client: option in place of api:.
  • Custom-adapter path: define a tiny module implementing ConfigCat.HTTPClient, pass it via http_client:, and verify ConfigFetcher works without HTTPoison being called. The Mox-backed ConfigCat.MockAPI (now mocking ConfigCat.HTTPClient) is a ready-made example.
  • HTTPoison-missing path: remove :httpoison from a consumer app's deps without setting :http_client — startup raises a clear ArgumentError instructing the user to either add HTTPoison or provide their own client.
  • Error classification: stub adapter returning {:error, %{reason: :timeout, transient?: true}} and {:error, %{reason: :unauthorized, transient?: false}}; verify the fetcher's retry/log behavior matches the previous HTTPoison-coupled implementation.

Security

The SDK still only issues outbound HTTPS requests to the ConfigCat CDN. Custom adapters run in the user's own application process and inherit whatever TLS/proxy posture they configure for their HTTP client — that boundary is now explicit (a documented behaviour) rather than implicit (HTTPoison/hackney options leaked through the SDK).

Requirement checklist

  • I have covered the applied changes with automated tests.
  • I have executed the full automated test set against my changes.
  • I have validated my changes against all supported platform versions.
  • I have read and accepted the contribution agreement.

up2jj added 6 commits May 12, 2026 16:38
Introduce a `ConfigCat.HTTPClient` behaviour with a normalized request
contract (plain maps for response/error, no HTTPoison structs at the
boundary). `ConfigCat.API` becomes the default adapter wrapping
HTTPoison and classifying transient vs. permanent errors. Callers can
swap in Finch, Req, Mint, Tesla or a test stub via the new
`:http_client` start option, which the supervisor plumbs through to
the config fetcher.
HTTPoison and Finch are both declared as optional deps. ConfigCat.API
guards itself with Code.ensure_loaded?/1 and raises a clear error if
neither HTTPoison nor a custom :http_client is configured.
ConfigCat.HTTPClient.Finch is a drop-in adapter that translates the
SDK's HTTPoison-shaped timeout options to Finch's request options and
reads the pool name from application config.
Pattern matching on %HTTPoison.Response{} / %HTTPoison.Error{} in
ConfigCat.API and %Finch.Response{} in ConfigCat.HTTPClient.Finch
forced the compiler to resolve those structs at compile time, so the
SDK could not compile when the optional dep was absent. Match on the
underlying map shape instead — same runtime semantics, no compile-time
coupling to the optional package.
Group the two built-in adapters under one namespace so they're
discoverable side-by-side in HexDocs:

  ConfigCat.HTTPClient.HTTPoison  (default)
  ConfigCat.HTTPClient.Finch
@up2jj up2jj requested a review from a team as a code owner May 13, 2026 06:13
@kp-cat
Copy link
Copy Markdown
Member

kp-cat commented May 21, 2026

Hey @up2jj,
Thank you for the PR.
It looks like some CI actions didn’t pass. Could you please check them?

Finch 0.18+ requires Elixir ~> 1.15, causing compile failures on the
1.12 and 1.14 CI jobs. Since we are contributors rather than maintainers
we cannot bump the project's minimum Elixir version, so remove the Finch
adapter and its dependency entirely. Users who need Finch can implement
the ConfigCat.HTTPClient behaviour themselves.

Also fix a formatting issue in config_fetcher.ex (guard clause line
wrapping) that caused the Elixir 1.16 format check to fail.
@up2jj up2jj force-pushed the pluggable-http-client branch from 9a516d3 to f10cadf Compare May 28, 2026 06:46
@up2jj
Copy link
Copy Markdown
Author

up2jj commented May 28, 2026

Hey @up2jj, Thank you for the PR. It looks like some CI actions didn’t pass. Could you please check them?

Hey @kp-cat I had to drop the support for Finch as it is not supported on Elixir 1.12. As long as you want to support this version SDK, Finch adapter cannot be shipped together due to failing check. Users can still implement it on their own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants