Skip to content

feat: Expose the Seam middleware for a caller built client - #472

Merged
razor-x merged 1 commit into
betafrom
claude/php-audit-uoa7nb-h5-from-client-middleware
Aug 19, 2026
Merged

feat: Expose the Seam middleware for a caller built client#472
razor-x merged 1 commit into
betafrom
claude/php-audit-uoa7nb-h5-from-client-middleware

Conversation

@razor-x

@razor-x razor-x commented Aug 17, 2026

Copy link
Copy Markdown
Member

Reworked per review, and rebased onto beta at a2cd8ce.

Previously this PR made from_client install the SDK's middleware on an injected client. That was the wrong pattern: a passed client should be left as given, and nothing stopped the middleware being applied twice — from_client($seam->client) or a stack already carrying seam_retry would have ended up with two sets of retries.

Now the behaviour is unchanged and opt-in, with the middleware made public.

The gap being closed

A client passed with the client option is used exactly as given, so it does not carry the SDK's error mapping or retries. An API error raises Guzzle's exception rather than Seam\HttpApiError, and a retryable failure is not retried. Nothing said so, and the README's own from_client example is subject to it.

The fix

Seam\Http\ClientFactory::add_middleware puts the error mapping and retry middleware on a handler stack the caller builds their client with:

$handler = GuzzleHttp\HandlerStack::create();

Seam\Http\ClientFactory::add_middleware($handler);

$client = new GuzzleHttp\Client([
    "base_uri" => "https://connect.getseam.com",
    "headers" => ["authorization" => "Bearer " . $api_key],
    "handler" => $handler,
    "http_errors" => false,
]);

$seam = Seam\Seam::from_client($client);

Guzzle fixes the handler stack at construction, so the middleware genuinely cannot be added afterwards — that is why this is a stack helper rather than something that takes a client.

ClientFactory::create now calls the same method, so there is a single definition of the order the middleware goes on in (error unshifted to the outside, retry pushed). retries is accepted and defaults to DEFAULT_RETRIES.

Applying it twice stacks two sets of retries. Since the caller owns the stack, that is documented as once-per-stack rather than guarded — happy to add a guard keyed on the seam_error/seam_retry names if you'd rather it be enforced.

Docs

New Advanced Usage section, "Adding the Seam middleware to your own client". "Overriding the client" now states plainly that the client is used as given and does not gain error mapping or retries.

Also corrected the standing claim that $seam->client "is the Guzzle client" — it is a Seam\Http\SerializingClient implementing ClientInterface, so instanceof Client checks and Guzzle-specific methods on it fail. Fixed in the README and both class docblocks.

Tests

Four in tests/ClientTest.php, all using a freshly built GuzzleHttp\Client rather than one taken from another Seam instance — every pre-existing test passed $seam->client, which is why the gap was invisible:

  • testAnInjectedClientIsUsedAsGiven — sends fine, and a 404 raises Guzzle's ClientException, pinning the documented behaviour.
  • testAddMiddlewareGivesAnInjectedClientSeamErrors — with the middleware, that 404 becomes HttpApiError.
  • testAddMiddlewareGivesAnInjectedClientRetries — 3 attempts on a 503.
  • testAddMiddlewareHonoursARetryCountretries: 0 gives 1 attempt.

Verified the three opt-in tests genuinely depend on the helper: with add_middleware's body disabled, all three fail (TypeError/HttpApiError not raised, attempt counts wrong).

Full suite: 232 tests green, psalm clean, npm run lint clean. The deprecations in the run are the resource-constructor issue from #473, not yet on beta.


Generated by Claude Code

A client passed with the client option is used exactly as given, which
means it does not carry the SDK's error mapping or retries: an API error
raises Guzzle's exception rather than HttpApiError, and a retryable
failure is not retried. Nothing said so, and the README's own
from_client example is subject to it.

Leave that behavior alone and make it opt in instead. Guzzle fixes the
handler stack when a client is constructed, so the middleware cannot be
added afterwards; ClientFactory::add_middleware puts it on a stack the
caller builds the client with. ClientFactory::create now uses the same
method, so there is one definition of the order the middleware goes on
in.

Applying it twice would stack two sets of retries, so it is documented
as once per stack rather than guarded, since the caller owns the stack.

Also correct the claim that $seam->client is the Guzzle client. It is a
SerializingClient implementing ClientInterface, so Guzzle specific calls
on it fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH3wdHh4Y6Wjyc5uHwk5iG
@razor-x
razor-x force-pushed the claude/php-audit-uoa7nb-h5-from-client-middleware branch from f1f7e42 to 7baefe9 Compare August 19, 2026 21:32
@razor-x razor-x changed the title fix: Keep error mapping and retries on an injected client feat: Expose the Seam middleware for a caller built client Aug 19, 2026
@razor-x
razor-x merged commit 7999a86 into beta Aug 19, 2026
15 checks passed
@razor-x
razor-x deleted the claude/php-audit-uoa7nb-h5-from-client-middleware branch August 19, 2026 22:17
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