From e16a17059a49a50c0152576feff3328d7dacc39f Mon Sep 17 00:00:00 2001 From: Cbrad24 Date: Fri, 16 Oct 2026 12:21:55 +1000 Subject: [PATCH 1/2] Use the request's API version in URL mode when the connector has one Co-Authored-By: Claude Opus 5.5 --- src/Traits/Plugins/HasApiVersion.php | 12 +++++++++++- tests/Unit/Concerns/HasApiVersionTest.php | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Traits/Plugins/HasApiVersion.php b/src/Traits/Plugins/HasApiVersion.php index 5be6e8e7..6e6287e7 100644 --- a/src/Traits/Plugins/HasApiVersion.php +++ b/src/Traits/Plugins/HasApiVersion.php @@ -61,11 +61,21 @@ public function bootHasApiVersion(PendingRequest $pendingRequest): void VersionMode::Header => $pendingRequest->headers()->add($this->versionKey, $this->apiVersion), VersionMode::QueryParam => $pendingRequest->query()->add($this->versionKey, $this->apiVersion), VersionMode::Url => $pendingRequest->setUrl( - str_replace('{version}', $this->ensureVersionIsSafeForUrl($this->apiVersion), $pendingRequest->getUrl()) + str_replace('{version}', $this->ensureVersionIsSafeForUrl($this->getRequestApiVersion($pendingRequest) ?? $this->apiVersion), $pendingRequest->getUrl()) ), }; } + /** + * Get the request's API version, which takes precedence in the URL because the connector boots first and replaces the placeholder before the request can. + */ + protected function getRequestApiVersion(PendingRequest $pendingRequest): ?string + { + $request = $pendingRequest->getRequest(); + + return method_exists($request, 'getApiVersion') ? $request->getApiVersion() : null; + } + /** * Ensure the version can only ever form a single subdomain label or path segment. * diff --git a/tests/Unit/Concerns/HasApiVersionTest.php b/tests/Unit/Concerns/HasApiVersionTest.php index b7f614a9..c1296b91 100644 --- a/tests/Unit/Concerns/HasApiVersionTest.php +++ b/tests/Unit/Concerns/HasApiVersionTest.php @@ -138,6 +138,19 @@ return true; }); }); + + test('the request can override the connector api version in the url', function () { + $mockClient = sendVersionedRequest( + makeVersionedConnector('https://api.provider.com/{version}', VersionMode::Url, 'v1'), + makeVersionedRequest('beta', VersionMode::Url), + ); + + $mockClient->assertSent(function (Request $request, $response) { + expect($response->getPendingRequest()->getUrl())->toBe('https://api.provider.com/beta'); + + return true; + }); + }); }); describe('Without A Version', function () { From 561500af3d46ad48035b0fb0d6736ea3305c8fe1 Mon Sep 17 00:00:00 2001 From: Cbrad24 Date: Sun, 18 Oct 2026 15:54:53 +1000 Subject: [PATCH 2/2] Use the request's URL version only when it uses HasApiVersion in URL mode Co-Authored-By: Claude Opus 5.5 --- src/Traits/Plugins/HasApiVersion.php | 30 ++++++++++++--- tests/Unit/Concerns/HasApiVersionTest.php | 46 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/Traits/Plugins/HasApiVersion.php b/src/Traits/Plugins/HasApiVersion.php index 6e6287e7..2e3e2185 100644 --- a/src/Traits/Plugins/HasApiVersion.php +++ b/src/Traits/Plugins/HasApiVersion.php @@ -4,6 +4,7 @@ namespace Saloon\Traits\Plugins; +use Saloon\Helpers\Helpers; use InvalidArgumentException; use Saloon\Enums\VersionMode; use Saloon\Http\PendingRequest; @@ -48,6 +49,14 @@ public function getApiVersion(): ?string return $this->apiVersion; } + /** + * Get where the API version is applied to the request + */ + public function getVersionMode(): VersionMode + { + return $this->versionMode; + } + /** * Boot HasApiVersion plugin. */ @@ -61,19 +70,30 @@ public function bootHasApiVersion(PendingRequest $pendingRequest): void VersionMode::Header => $pendingRequest->headers()->add($this->versionKey, $this->apiVersion), VersionMode::QueryParam => $pendingRequest->query()->add($this->versionKey, $this->apiVersion), VersionMode::Url => $pendingRequest->setUrl( - str_replace('{version}', $this->ensureVersionIsSafeForUrl($this->getRequestApiVersion($pendingRequest) ?? $this->apiVersion), $pendingRequest->getUrl()) + str_replace('{version}', $this->ensureVersionIsSafeForUrl($this->resolveUrlApiVersion($pendingRequest)), $pendingRequest->getUrl()) ), }; } /** - * Get the request's API version, which takes precedence in the URL because the connector boots first and replaces the placeholder before the request can. + * Resolve which version to substitute into `{version}` for URL mode. */ - protected function getRequestApiVersion(PendingRequest $pendingRequest): ?string + protected function resolveUrlApiVersion(PendingRequest $pendingRequest): string { - $request = $pendingRequest->getRequest(); + if ($pendingRequest->getConnector() === $this) { + $request = $pendingRequest->getRequest(); - return method_exists($request, 'getApiVersion') ? $request->getApiVersion() : null; + if (in_array(HasApiVersion::class, Helpers::classUsesRecursive($request), true) + && $request->getVersionMode() === VersionMode::Url) { + $requestVersion = $request->getApiVersion(); + + if (is_string($requestVersion) && $requestVersion !== '') { + return $requestVersion; + } + } + } + + return $this->apiVersion; } /** diff --git a/tests/Unit/Concerns/HasApiVersionTest.php b/tests/Unit/Concerns/HasApiVersionTest.php index c1296b91..bbd6d9aa 100644 --- a/tests/Unit/Concerns/HasApiVersionTest.php +++ b/tests/Unit/Concerns/HasApiVersionTest.php @@ -151,6 +151,52 @@ return true; }); }); + + test('a request versioned by header leaves the connector api version in the url', function () { + $mockClient = sendVersionedRequest( + makeVersionedConnector('https://api.provider.com/{version}', VersionMode::Url, 'v1'), + makeVersionedRequest('beta', VersionMode::Header), + ); + + $mockClient->assertSent(function (Request $request, $response) { + expect($response->getPendingRequest()->getUrl())->toBe('https://api.provider.com/v1'); + expect($response->getPsrRequest()->getHeaderLine('api-version'))->toBe('beta'); + + return true; + }); + }); + + test('a request with its own version methods but not the trait leaves the connector api version in the url', function () { + $request = new class extends Request { + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return ''; + } + + public function getApiVersion(): string + { + return 'beta'; + } + + public function getVersionMode(): VersionMode + { + return VersionMode::Url; + } + }; + + $mockClient = sendVersionedRequest( + makeVersionedConnector('https://api.provider.com/{version}', VersionMode::Url, 'v1'), + $request, + ); + + $mockClient->assertSent(function (Request $request, $response) { + expect($response->getPendingRequest()->getUrl())->toBe('https://api.provider.com/v1'); + + return true; + }); + }); }); describe('Without A Version', function () {