Skip to content

Pluggable HTTP client#149

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

Pluggable HTTP client#149
up2jj wants to merge 6 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, opt into the new built-in Finch adapter, or supply their own client (Req, Mint, 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.
  • Two built-in adapters live side-by-side under one namespace:
    • ConfigCat.HTTPClient.HTTPoison — the default, 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.)
    • ConfigCat.HTTPClient.Finch — drop-in Finch adapter. Reads the pool name from config :configcat, ConfigCat.HTTPClient.Finch, name: MyApp.Finch, translates the SDK's HTTPoison-shaped timeout options into Finch request options.
  • :httpoison and :finch are now declared optional: true in mix.exs. Consumer apps add whichever one they want — or neither, if they ship their own adapter.
  • Both adapters match on map shape (%{status_code: ...}, %{status: ...}) 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 all three paths (default, Finch, custom); 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.
  • Finch path: add {:finch, "~> 0.18"}, start {Finch, name: MyApp.Finch} in the supervision tree, configure config :configcat, ConfigCat.HTTPClient.Finch, name: MyApp.Finch, and pass http_client: ConfigCat.HTTPClient.Finch to ConfigCat.start_link/1. Verified end-to-end against the ConfigCat CDN with a fresh consumer app that has no HTTPoison in its deps: the SDK compiled, fetched a live config, and evaluated flags successfully.
  • 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
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.

1 participant