Skip to content

[SPIKE] PAT-1866 Symfony HttpClient instead of Guzzle (base + git-service)#517

Draft
pepamartinec wants to merge 5 commits into
mainfrom
pepa/symfony-httpclient-spike
Draft

[SPIKE] PAT-1866 Symfony HttpClient instead of Guzzle (base + git-service)#517
pepamartinec wants to merge 5 commits into
mainfrom
pepa/symfony-httpclient-spike

Conversation

@pepamartinec

Copy link
Copy Markdown
Contributor

⚠️ Spike — not for merge

Throwaway exploration for PAT-1866: what would adopting Symfony HttpClient instead of Guzzle do for php-api-client-base?

https://linear.app/keboola/issue/PAT-1866/unified-api-client-base

This branch is based on the git-service migration branch (pepa/common-api-lib-git-service, PR #516) on purpose, so the diff below is exactly the Guzzle → Symfony HttpClient swap for the base lib + git-service — the side-by-side comparison. The Guzzle versions to compare against are PRs #513 (base) and #516 (git-service).

Both libs are composer ci green on Symfony HttpClient (base 34 tests, git-service 45 tests, PHPStan level max with zero @phpstan-ignore in src/). Full writeup: libs/php-api-client-base/SPIKE_NOTES.md.

What Symfony HttpClient lets us delete

  • RetryDecider (67 LOC) → RetryableHttpClient + a 37-LOC strategy factory (backoff/jitter/attempt-counting/retry-logging free).
  • Json.php$response->toArray() + ['json' => …] request bodies.
  • Guzzle log middleware → RetryableHttpClient logs retries.
  • Dropped both guzzlehttp/guzzle and psr/http-message.
  • MockHttpClient / MockResponse testing (records the request; getRequestsCount() for retry asserts).

What changes (breaking — absorbed in the base once, then re-migrate the fleet)

  • Auth contract: PSR-7 decorator → header map getAuthenticationHeaders(): array, applied by an AuthenticatingHttpClient decorator wrapped inside RetryableHttpClient (so auth re-runs per retry attempt — covered by a test).
  • Request API: PSR-7 Request(method, path, options) (facade methods became one-liners).
  • Test seam: requestHandler → inject a ?HttpClientInterface (MockHttpClient).
  • Response laziness (force getContent() to surface errors); Symfony default retry is idempotent-only (we pass a flat 5xx list to match old behavior).

Why it's interesting for us

Keboola services run on Symfony, so they'd get an autowired HttpClientInterface, framework.http_client.scoped_clients config, and profiler/TraceableHttpClient observability for free. The flat-scalar facade DI shape is unaffected by the choice — only the internal authenticator contract + request-building change.

Bottom line

Worth adopting for new clients and Symfony-hosted services, but it's an API + contract break — not a drop-in swap. See SPIKE_NOTES.md for the full recommendation.

Replace the duplicated transport/auth/retry/JSON plumbing (git-service was
the reference the base lib was extracted from) with the shared base. The
GitServiceApiClient facade takes the Manage API token + tunable options as
flat constructor params; a null token defaults to service-account auth (the
projected SA token), preserving the original default behavior. The
{code, error} error format is a standalone GitServiceErrorMessageResolver.
Replace the Guzzle transport with symfony/http-client:

- ApiClient wraps HttpClientInterface; requests issued as
  (method, path, options) instead of PSR-7 Request objects.
- Auth contract becomes a header map (getAuthenticationHeaders());
  AuthenticatingHttpClient decorator merges them per request, wrapped
  inside RetryableHttpClient so auth re-resolves on every retry.
- Retry handled by Symfony GenericRetryStrategy via RetryStrategyFactory
  (transport errors + all 5xx + configured codes, for all methods).
- Drop Json + RetryDecider; use response toArray()/getContent() and the
  json request option. DefaultErrorMessageResolver decodes the body itself.
- ApiClientOptions swaps the Guzzle requestHandler for an injectable
  HttpClientInterface test seam.
- Tests rewritten on MockHttpClient/MockResponse.
- Swap guzzlehttp/guzzle + psr/http-message for symfony/http-client in
  composer.json; the base lib now provides the Symfony transport.
- Facade methods call sendRequest/sendRequestAndMapResponse with
  (method, path, options) instead of building PSR-7 Request objects;
  JSON bodies pass via the 'json' request option.
- Constructor test seam becomes ?HttpClientInterface $httpClient (was the
  Guzzle requestHandler); auth logic unchanged.
- GitServiceErrorMessageResolver json_decodes the body directly (Json helper
  removed from the base lib).
- Tests rewritten on MockHttpClient/MockResponse.
@linear

linear Bot commented Jun 10, 2026

Copy link
Copy Markdown

PAT-1866

@pepamartinec pepamartinec force-pushed the pepa/common-api-lib-git-service branch 3 times, most recently from f90cd5b to f6662fe Compare June 17, 2026 08:44
@pepamartinec pepamartinec force-pushed the pepa/common-api-lib-git-service branch from f6662fe to 93bf863 Compare June 19, 2026 08:36
Base automatically changed from pepa/common-api-lib-git-service to main June 19, 2026 08:41
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