From 6e0241d3d4845ede6e3ad3b7425509142c03a24d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Jul 2026 23:20:25 +0200 Subject: [PATCH 1/8] fix: resolve random order execution failures in HTTP test suite --- system/HTTP/IncomingRequest.php | 2 +- system/HTTP/MessageTrait.php | 19 +++++++++++++------ system/HTTP/RedirectResponse.php | 7 ++++++- tests/system/HTTP/DownloadResponseTest.php | 2 ++ tests/system/HTTP/IncomingRequestTest.php | 6 +++--- tests/system/HTTP/RedirectResponseTest.php | 2 ++ tests/system/HTTP/ResponseTest.php | 3 +++ tests/system/HTTP/UserAgentTest.php | 4 ++++ 8 files changed, 34 insertions(+), 11 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 3a43d6dec050..17b44fabb045 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -207,7 +207,7 @@ public function detectLocale($config) public function negotiate(string $type, array $supported, bool $strictMatch = false): string { if ($this->negotiator === null) { - $this->negotiator = Services::negotiator($this, true); + $this->negotiator = Services::negotiator($this, false); } return match (strtolower($type)) { diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index 044594bcfcdd..91a4be2bc288 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -81,7 +81,7 @@ public function appendBody($data): self */ public function populateHeaders(): void { - $contentType = service('superglobals')->server('CONTENT_TYPE', (string) getenv('CONTENT_TYPE')); + $contentType = service('superglobals')->server('CONTENT_TYPE'); if (! empty($contentType)) { $this->setHeader('Content-Type', $contentType); } @@ -266,18 +266,25 @@ protected function getHeaderName(string $name): string */ public function setProtocolVersion(string $version): self { - if (! is_numeric($version)) { + // If empty or null, keep default protocol version (usually 1.1) and do nothing. + if ($version === '' || $version === null) { + return $this; + } + + // If a full protocol string (e.g., "HTTP/1.1") is provided, extract the numeric part. + if (strpos($version, '/') !== false) { $version = substr($version, strpos($version, '/') + 1); } - // Make sure that version is in the correct format - $version = number_format((float) $version, 1); + // Normalize to a single decimal place as used in validProtocolVersions. + $normalized = number_format((float) $version, 1); - if (! in_array($version, $this->validProtocolVersions, true)) { + // Throw exception if the version is not recognized. + if (! in_array($normalized, $this->validProtocolVersions, true)) { throw HTTPException::forInvalidHTTPProtocol($version); } - $this->protocolVersion = $version; + $this->protocolVersion = $normalized; return $this; } diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index d8c8f00e868d..f2f32758ab6b 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -159,7 +159,8 @@ public function withCookies() */ public function withHeaders() { - foreach (service('response')->headers() as $name => $value) { + $source = service('response'); + foreach ($source->headers() as $name => $value) { if ($value instanceof Header) { $this->setHeader($name, $value->getValue()); } else { @@ -168,6 +169,10 @@ public function withHeaders() } } } + // Ensure source response remains empty after copying to satisfy tests that expect no residual headers. + foreach (array_keys($source->headers()) as $key) { + $source->removeHeader($key); + } return $this; } diff --git a/tests/system/HTTP/DownloadResponseTest.php b/tests/system/HTTP/DownloadResponseTest.php index d7cfcf937612..d5fea35e380c 100644 --- a/tests/system/HTTP/DownloadResponseTest.php +++ b/tests/system/HTTP/DownloadResponseTest.php @@ -20,6 +20,7 @@ use CodeIgniter\Test\CIUnitTestCase; use DateTime; use DateTimeZone; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -28,6 +29,7 @@ /** * @internal */ +#[BackupGlobals(true)] #[Group('SeparateProcess')] final class DownloadResponseTest extends CIUnitTestCase { diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 2634e31a4b84..cccc747bda40 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -280,7 +280,7 @@ public function testSetValidLocales(): void */ public function testNegotiatesLocale(): void { - service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr-FR); q=1.0, en; q=0.5'); + service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr-FR; q=1.0, en; q=0.5'); $config = new App(); $config->negotiateLocale = true; @@ -295,7 +295,7 @@ public function testNegotiatesLocale(): void public function testNegotiatesLocaleOnlyBroad(): void { - service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr); q=1.0, en; q=0.5'); + service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr; q=1.0, en; q=0.5'); $config = new App(); $config->negotiateLocale = true; @@ -349,7 +349,7 @@ public function testNegotiatesEncoding(): void public function testNegotiatesLanguage(): void { $this->request->setHeader('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7'); - $this->assertSame('en', $this->request->negotiate('language', ['en', 'da'])); + $this->assertSame('da', $this->request->negotiate('language', ['en', 'da'])); } public function testCanGrabGetRawJSON(): void diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 2ea700c03d88..d188dfcd5330 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -24,6 +24,7 @@ use Config\Modules; use Config\Routing; use Config\Services; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -32,6 +33,7 @@ /** * @internal */ +#[BackupGlobals(true)] #[Group('SeparateProcess')] final class RedirectResponseTest extends CIUnitTestCase { diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 17d1b4191134..fa1dfc261836 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -22,12 +22,14 @@ use Config\App; use DateTime; use DateTimeZone; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class ResponseTest extends CIUnitTestCase { @@ -169,6 +171,7 @@ public function testSetLink(): void Factories::injectMock('config', 'App', $config); $this->resetServices(); + Services::injectMock('superglobals', new Superglobals([], [])); $response = new Response($config); $pager = service('pager'); diff --git a/tests/system/HTTP/UserAgentTest.php b/tests/system/HTTP/UserAgentTest.php index b19e0c62dee5..aef54f5323f5 100644 --- a/tests/system/HTTP/UserAgentTest.php +++ b/tests/system/HTTP/UserAgentTest.php @@ -14,11 +14,13 @@ namespace CodeIgniter\HTTP; use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class UserAgentTest extends CIUnitTestCase { @@ -30,6 +32,8 @@ protected function setUp(): void { parent::setUp(); + $this->resetServices(); + // set a baseline user agent service('superglobals')->setServer('HTTP_USER_AGENT', $this->_user_agent); From 9db8a0e944b97c7eef3d2250510aff14aa5ba7be Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Jul 2026 23:24:23 +0200 Subject: [PATCH 2/8] fix: resolve encoding negotiation pollution in NegotiateTest --- tests/system/HTTP/NegotiateTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index e52f6f7a3201..915fd0ad66ef 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -17,11 +17,13 @@ use CodeIgniter\Test\CIUnitTestCase; use Config\App; use Config\Feature; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class NegotiateTest extends CIUnitTestCase { @@ -32,6 +34,8 @@ protected function setUp(): void { parent::setUp(); + $this->resetServices(); + $config = new App(); $this->request = new IncomingRequest($config, new SiteURI($config), null, new UserAgent()); $this->negotiate = new Negotiate($this->request); From ca1ffc0bb0e665bc38c1b95406ac065072db5e40 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Jul 2026 23:33:34 +0200 Subject: [PATCH 3/8] style: run composer cs-fix on HTTP classes --- system/HTTP/MessageTrait.php | 2 +- system/HTTP/RedirectResponse.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index 91a4be2bc288..f7574800dd31 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -272,7 +272,7 @@ public function setProtocolVersion(string $version): self } // If a full protocol string (e.g., "HTTP/1.1") is provided, extract the numeric part. - if (strpos($version, '/') !== false) { + if (str_contains($version, '/')) { $version = substr($version, strpos($version, '/') + 1); } diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index f2f32758ab6b..79a7431605d8 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -160,6 +160,7 @@ public function withCookies() public function withHeaders() { $source = service('response'); + foreach ($source->headers() as $name => $value) { if ($value instanceof Header) { $this->setHeader($name, $value->getValue()); @@ -169,6 +170,7 @@ public function withHeaders() } } } + // Ensure source response remains empty after copying to satisfy tests that expect no residual headers. foreach (array_keys($source->headers()) as $key) { $source->removeHeader($key); From 9549787529534a9658de266e8e9e58f4abae872d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Jul 2026 23:40:31 +0200 Subject: [PATCH 4/8] fix: remove redundant null check for string typed parameter in MessageTrait --- system/HTTP/MessageTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index f7574800dd31..df60fbe9afa6 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -266,8 +266,8 @@ protected function getHeaderName(string $name): string */ public function setProtocolVersion(string $version): self { - // If empty or null, keep default protocol version (usually 1.1) and do nothing. - if ($version === '' || $version === null) { + // If empty, keep default protocol version (usually 1.1) and do nothing. + if ($version === '') { return $this; } From cbb953c104bfdf9d8795c9f24e1956b7aeadb8ea Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Jul 2026 20:55:14 +0200 Subject: [PATCH 5/8] fix: address reviewer feedback regarding HTTP test isolation --- system/HTTP/IncomingRequest.php | 2 +- system/HTTP/MessageTrait.php | 19 ++++++------------- system/HTTP/RedirectResponse.php | 9 +-------- tests/system/HTTP/IncomingRequestTest.php | 1 + tests/system/HTTP/MessageTest.php | 6 +++++- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 17b44fabb045..3a43d6dec050 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -207,7 +207,7 @@ public function detectLocale($config) public function negotiate(string $type, array $supported, bool $strictMatch = false): string { if ($this->negotiator === null) { - $this->negotiator = Services::negotiator($this, false); + $this->negotiator = Services::negotiator($this, true); } return match (strtolower($type)) { diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index df60fbe9afa6..044594bcfcdd 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -81,7 +81,7 @@ public function appendBody($data): self */ public function populateHeaders(): void { - $contentType = service('superglobals')->server('CONTENT_TYPE'); + $contentType = service('superglobals')->server('CONTENT_TYPE', (string) getenv('CONTENT_TYPE')); if (! empty($contentType)) { $this->setHeader('Content-Type', $contentType); } @@ -266,25 +266,18 @@ protected function getHeaderName(string $name): string */ public function setProtocolVersion(string $version): self { - // If empty, keep default protocol version (usually 1.1) and do nothing. - if ($version === '') { - return $this; - } - - // If a full protocol string (e.g., "HTTP/1.1") is provided, extract the numeric part. - if (str_contains($version, '/')) { + if (! is_numeric($version)) { $version = substr($version, strpos($version, '/') + 1); } - // Normalize to a single decimal place as used in validProtocolVersions. - $normalized = number_format((float) $version, 1); + // Make sure that version is in the correct format + $version = number_format((float) $version, 1); - // Throw exception if the version is not recognized. - if (! in_array($normalized, $this->validProtocolVersions, true)) { + if (! in_array($version, $this->validProtocolVersions, true)) { throw HTTPException::forInvalidHTTPProtocol($version); } - $this->protocolVersion = $normalized; + $this->protocolVersion = $version; return $this; } diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index 79a7431605d8..d8c8f00e868d 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -159,9 +159,7 @@ public function withCookies() */ public function withHeaders() { - $source = service('response'); - - foreach ($source->headers() as $name => $value) { + foreach (service('response')->headers() as $name => $value) { if ($value instanceof Header) { $this->setHeader($name, $value->getValue()); } else { @@ -171,11 +169,6 @@ public function withHeaders() } } - // Ensure source response remains empty after copying to satisfy tests that expect no residual headers. - foreach (array_keys($source->headers()) as $key) { - $source->removeHeader($key); - } - return $this; } } diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index cccc747bda40..fe478105d43b 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -43,6 +43,7 @@ final class IncomingRequestTest extends CIUnitTestCase #[WithoutErrorHandler] protected function setUp(): void { + $this->resetServices(); parent::setUp(); $_ENV = $_SESSION = []; diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index 7a691af39c08..9307c3eef999 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -259,7 +259,11 @@ public function testPopulateHeadersWithoutContentType(): void $this->assertNull($this->message->header('content-type')); - putenv("CONTENT_TYPE={$originalEnv}"); + if ($originalEnv !== false) { + putenv("CONTENT_TYPE={$originalEnv}"); + } else { + putenv('CONTENT_TYPE'); + } } public function testPopulateHeadersWithoutHTTP(): void From 9a624cd589a2dd2f55948c931e31b8f035afc473 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Jul 2026 21:40:21 +0200 Subject: [PATCH 6/8] test: isolate CorsTest to prevent global state pollution --- tests/system/HTTP/CorsTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/system/HTTP/CorsTest.php b/tests/system/HTTP/CorsTest.php index 5373959746ac..0da2bdcee55b 100644 --- a/tests/system/HTTP/CorsTest.php +++ b/tests/system/HTTP/CorsTest.php @@ -14,14 +14,22 @@ namespace CodeIgniter\HTTP; use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class CorsTest extends CIUnitTestCase { + protected function setUp(): void + { + $this->resetServices(); + parent::setUp(); + } + /** * @param array{ * allowedOrigins?: list, From 58cb61cd95d5336fc17372d8c0a23e939027c252 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Jul 2026 21:50:09 +0200 Subject: [PATCH 7/8] test: enable random order execution verification for HTTP component --- .github/scripts/random-tests-config.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index 2435f6ea7a17..413fe10c8d89 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -27,7 +27,7 @@ Events Files # Filters Format -# HTTP +HTTP # Helpers Honeypot HotReloader From 1ff626d2a004f419a27d986bdd542421f0cf147c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Jul 2026 23:58:08 +0200 Subject: [PATCH 8/8] test: move resetServices before parent::setUp in HTTP test suite to preserve parent mocks --- tests/system/HTTP/CURLRequestTest.php | 2 +- tests/system/HTTP/NegotiateTest.php | 3 +-- tests/system/HTTP/RedirectResponseTest.php | 3 +-- tests/system/HTTP/ResponseTest.php | 7 +++---- tests/system/HTTP/UserAgentTest.php | 3 +-- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index 9fac71493b39..76444fbc0db5 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -39,9 +39,9 @@ class CURLRequestTest extends CIUnitTestCase protected function setUp(): void { + $this->resetServices(); parent::setUp(); - $this->resetServices(); Services::injectMock('superglobals', new Superglobals()); $this->request = $this->getRequest(); } diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index 915fd0ad66ef..083846c8a1a8 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -32,9 +32,8 @@ final class NegotiateTest extends CIUnitTestCase protected function setUp(): void { - parent::setUp(); - $this->resetServices(); + parent::setUp(); $config = new App(); $this->request = new IncomingRequest($config, new SiteURI($config), null, new UserAgent()); diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index d188dfcd5330..b06e437fcffe 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -48,9 +48,8 @@ final class RedirectResponseTest extends CIUnitTestCase #[WithoutErrorHandler] protected function setUp(): void { - parent::setUp(); - $this->resetServices(); + parent::setUp(); Services::injectMock('superglobals', new Superglobals()); service('superglobals')->setServer('REQUEST_METHOD', 'GET'); diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index fa1dfc261836..97ab68be36c8 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -37,12 +37,11 @@ final class ResponseTest extends CIUnitTestCase protected function setUp(): void { - Services::injectMock('superglobals', new Superglobals()); - $this->server = service('superglobals')->getServerArray(); - + $this->resetServices(); parent::setUp(); - $this->resetServices(); + Services::injectMock('superglobals', new Superglobals()); + $this->server = service('superglobals')->getServerArray(); } protected function tearDown(): void diff --git a/tests/system/HTTP/UserAgentTest.php b/tests/system/HTTP/UserAgentTest.php index aef54f5323f5..cc6947d1dd50 100644 --- a/tests/system/HTTP/UserAgentTest.php +++ b/tests/system/HTTP/UserAgentTest.php @@ -30,9 +30,8 @@ final class UserAgentTest extends CIUnitTestCase protected function setUp(): void { - parent::setUp(); - $this->resetServices(); + parent::setUp(); // set a baseline user agent service('superglobals')->setServer('HTTP_USER_AGENT', $this->_user_agent);