From cde5bba05920423447f21b66c63b5998803540aa Mon Sep 17 00:00:00 2001 From: JonPurvis Date: Mon, 2 Mar 2026 19:30:05 +0000 Subject: [PATCH 1/5] swap header to use psrResponse --- src/Http/Response.php | 8 +++++++- tests/Unit/ResponseTest.php | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Http/Response.php b/src/Http/Response.php index 4d4485e3..f067cccc 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -486,7 +486,13 @@ public function throw(): static */ public function header(string $header): string|array|null { - return $this->headers()->get($header); + if (! $this->psrResponse->hasHeader($header)) { + return null; + } + + $values = $this->psrResponse->getHeader($header); + + return count($values) === 1 ? $values[0] : $values; } /** diff --git a/tests/Unit/ResponseTest.php b/tests/Unit/ResponseTest.php index 02d0e363..8fd195a1 100644 --- a/tests/Unit/ResponseTest.php +++ b/tests/Unit/ResponseTest.php @@ -472,3 +472,14 @@ $response = $connector->send(new UserRequest, $mockClient); expect($response->isXml())->toBeFalse(); }); + +test('header lookup is case-insensitive per HTTP RFC', function () { + $mockClient = new MockClient([ + MockResponse::make([], 200, ['x-my-custom-header' => 'custom-value']), + ]); + + $response = connector()->send(new UserRequest, $mockClient); + + expect($response->header('X-My-Custom-Header'))->toEqual('custom-value'); + expect($response->header('x-my-custom-header'))->toEqual('custom-value'); +}); From 3ab75aee3dcb57d0dcf053550769b14c233c2740 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:17:45 +0000 Subject: [PATCH 2/5] Improved checking of isJson and isXml and standardised the header lookup --- src/Helpers/Storage.php | 2 +- src/Helpers/URLHelper.php | 4 ++-- src/Http/Auth/TokenAuthenticator.php | 2 +- src/Http/Response.php | 23 +++++++++++++++----- src/Traits/OAuth2/AuthorizationCodeGrant.php | 2 +- tests/Unit/ResponseTest.php | 22 +++++++++++++++++++ 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/Helpers/Storage.php b/src/Helpers/Storage.php index db8eafba..01b332a1 100644 --- a/src/Helpers/Storage.php +++ b/src/Helpers/Storage.php @@ -48,7 +48,7 @@ protected function buildPath(string $path): string { $trimRules = DIRECTORY_SEPARATOR . ' '; - return rtrim($this->baseDirectory, $trimRules) . DIRECTORY_SEPARATOR . ltrim($path, $trimRules); + return mb_rtrim($this->baseDirectory, $trimRules) . DIRECTORY_SEPARATOR . mb_ltrim($path, $trimRules); } /** diff --git a/src/Helpers/URLHelper.php b/src/Helpers/URLHelper.php index 9622c481..4427ceb4 100644 --- a/src/Helpers/URLHelper.php +++ b/src/Helpers/URLHelper.php @@ -27,12 +27,12 @@ public static function join(string $baseUrl, string $endpoint): string } if ($endpoint !== '/') { - $endpoint = ltrim($endpoint, '/ '); + $endpoint = mb_ltrim($endpoint, '/ '); } $requiresTrailingSlash = ! empty($endpoint) && $endpoint !== '/'; - $baseEndpoint = rtrim($baseUrl, '/ '); + $baseEndpoint = mb_rtrim($baseUrl, '/ '); $baseEndpoint = $requiresTrailingSlash ? $baseEndpoint . '/' : $baseEndpoint; diff --git a/src/Http/Auth/TokenAuthenticator.php b/src/Http/Auth/TokenAuthenticator.php index 402ebe74..0c3bbf3f 100644 --- a/src/Http/Auth/TokenAuthenticator.php +++ b/src/Http/Auth/TokenAuthenticator.php @@ -22,6 +22,6 @@ public function __construct( */ public function set(PendingRequest $pendingRequest): void { - $pendingRequest->headers()->add('Authorization', trim($this->prefix . ' ' . $this->token)); + $pendingRequest->headers()->add('Authorization', mb_trim($this->prefix . ' ' . $this->token)); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index f067cccc..1500d2da 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -6,7 +6,10 @@ use Throwable; use LogicException; +use function implode; use SimpleXMLElement; +use function is_array; +use function mb_strtolower; use Saloon\Traits\Macroable; use InvalidArgumentException; use Saloon\Helpers\ArrayHelpers; @@ -500,13 +503,17 @@ public function header(string $header): string|array|null */ public function isJson(): bool { - $contentType = $this->psrResponse->getHeaderLine('Content-Type'); + $contentType = $this->header('Content-Type'); - if ($contentType === '') { + if (empty($contentType)) { return false; } - return str_contains($contentType, 'json'); + if (is_array($contentType)) { + $contentType = implode(',', $contentType); + } + + return str_contains(mb_strtolower($contentType), 'json'); } /** @@ -514,13 +521,17 @@ public function isJson(): bool */ public function isXml(): bool { - $contentType = $this->psrResponse->getHeaderLine('Content-Type'); + $contentType = $this->header('Content-Type'); - if ($contentType === '') { + if (empty($contentType)) { return false; } - return str_contains($contentType, 'xml'); + if (is_array($contentType)) { + $contentType = implode(',', $contentType); + } + + return str_contains(mb_strtolower($contentType), 'xml'); } /** diff --git a/src/Traits/OAuth2/AuthorizationCodeGrant.php b/src/Traits/OAuth2/AuthorizationCodeGrant.php index 8db18a81..aa31993c 100644 --- a/src/Traits/OAuth2/AuthorizationCodeGrant.php +++ b/src/Traits/OAuth2/AuthorizationCodeGrant.php @@ -58,7 +58,7 @@ public function getAuthorizationUrl(array $scopes = [], ?string $state = null, s ]); $query = http_build_query($queryParameters, '', '&', PHP_QUERY_RFC3986); - $query = trim($query, '?&'); + $query = mb_trim($query, '?&'); $url = URLHelper::join($this->resolveBaseUrl(), $config->getAuthorizeEndpoint()); diff --git a/tests/Unit/ResponseTest.php b/tests/Unit/ResponseTest.php index 8fd195a1..95fbc452 100644 --- a/tests/Unit/ResponseTest.php +++ b/tests/Unit/ResponseTest.php @@ -483,3 +483,25 @@ expect($response->header('X-My-Custom-Header'))->toEqual('custom-value'); expect($response->header('x-my-custom-header'))->toEqual('custom-value'); }); + +test('isJson lookup can use case insensitive headers', function () { + $mockClient = new MockClient([ + MockResponse::make([], 200, ['content-type' => 'application/JSON']), + ]); + + $response = connector()->send(new UserRequest, $mockClient); + + expect($response->isJson())->toBeTrue(); + expect($response->isXml())->toBeFalse(); +}); + +test('isXml lookup can use case insensitive headers', function () { + $mockClient = new MockClient([ + MockResponse::make([], 200, ['content-type' => 'text/xml']), + ]); + + $response = connector()->send(new UserRequest, $mockClient); + + expect($response->isXml())->toBeTrue(); + expect($response->isJson())->toBeFalse(); +}); From 5af1dd6dd6cfe33c2491e42812791169a6400b40 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:22:09 +0000 Subject: [PATCH 3/5] Revert Sammy's silly changes --- src/Helpers/Storage.php | 2 +- src/Helpers/URLHelper.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Helpers/Storage.php b/src/Helpers/Storage.php index 01b332a1..db8eafba 100644 --- a/src/Helpers/Storage.php +++ b/src/Helpers/Storage.php @@ -48,7 +48,7 @@ protected function buildPath(string $path): string { $trimRules = DIRECTORY_SEPARATOR . ' '; - return mb_rtrim($this->baseDirectory, $trimRules) . DIRECTORY_SEPARATOR . mb_ltrim($path, $trimRules); + return rtrim($this->baseDirectory, $trimRules) . DIRECTORY_SEPARATOR . ltrim($path, $trimRules); } /** diff --git a/src/Helpers/URLHelper.php b/src/Helpers/URLHelper.php index 4427ceb4..9622c481 100644 --- a/src/Helpers/URLHelper.php +++ b/src/Helpers/URLHelper.php @@ -27,12 +27,12 @@ public static function join(string $baseUrl, string $endpoint): string } if ($endpoint !== '/') { - $endpoint = mb_ltrim($endpoint, '/ '); + $endpoint = ltrim($endpoint, '/ '); } $requiresTrailingSlash = ! empty($endpoint) && $endpoint !== '/'; - $baseEndpoint = mb_rtrim($baseUrl, '/ '); + $baseEndpoint = rtrim($baseUrl, '/ '); $baseEndpoint = $requiresTrailingSlash ? $baseEndpoint . '/' : $baseEndpoint; From 874a4116324611658a1c9aa1c0f9ae9e3ddf051b Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:22:54 +0000 Subject: [PATCH 4/5] Fixed mb_trim issue --- src/Http/Auth/TokenAuthenticator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Auth/TokenAuthenticator.php b/src/Http/Auth/TokenAuthenticator.php index 0c3bbf3f..402ebe74 100644 --- a/src/Http/Auth/TokenAuthenticator.php +++ b/src/Http/Auth/TokenAuthenticator.php @@ -22,6 +22,6 @@ public function __construct( */ public function set(PendingRequest $pendingRequest): void { - $pendingRequest->headers()->add('Authorization', mb_trim($this->prefix . ' ' . $this->token)); + $pendingRequest->headers()->add('Authorization', trim($this->prefix . ' ' . $this->token)); } } From f268ac07412d2458e37c774da099c8e0f7d60a1f Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:23:55 +0000 Subject: [PATCH 5/5] Fixed another issue --- src/Traits/OAuth2/AuthorizationCodeGrant.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/OAuth2/AuthorizationCodeGrant.php b/src/Traits/OAuth2/AuthorizationCodeGrant.php index aa31993c..8db18a81 100644 --- a/src/Traits/OAuth2/AuthorizationCodeGrant.php +++ b/src/Traits/OAuth2/AuthorizationCodeGrant.php @@ -58,7 +58,7 @@ public function getAuthorizationUrl(array $scopes = [], ?string $state = null, s ]); $query = http_build_query($queryParameters, '', '&', PHP_QUERY_RFC3986); - $query = mb_trim($query, '?&'); + $query = trim($query, '?&'); $url = URLHelper::join($this->resolveBaseUrl(), $config->getAuthorizeEndpoint());