From eeb7ddefa191c4e8d8488511faa030ca895dcd9a Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 13 Jan 2026 14:36:13 +0100 Subject: [PATCH 1/9] feat(transport): use share handle --- src/HttpClient/HttpClient.php | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index 2a373d930..33d4d5d07 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -22,10 +22,44 @@ class HttpClient implements HttpClientInterface */ protected $sdkVersion; + /** + * Either a persistent share handle or a regular share handle, or null if no share handle can be obtained. + * + * @var object|resource|null + */ + private $shareHandle; + public function __construct(string $sdkIdentifier, string $sdkVersion) { $this->sdkIdentifier = $sdkIdentifier; $this->sdkVersion = $sdkVersion; + if (\function_exists('curl_share_init_persistent')) { + $shareOptions = [\CURL_LOCK_DATA_DNS, \CURL_LOCK_DATA_CONNECT]; + if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { + $shareOptions[] = \CURL_LOCK_DATA_SSL_SESSION; + } + try { + $this->shareHandle = curl_share_init_persistent($shareOptions); + } catch (\Throwable $throwable) { + // don't crash if the share handle cannot be created + } + } + + // If the persistent share handle cannot be created or doesn't exist + if ($this->shareHandle === null) { + try { + $this->shareHandle = curl_share_init(); + curl_share_setopt($this->shareHandle, \CURLOPT_SHARE, \CURL_LOCK_DATA_DNS); + if (\defined('CURL_LOCK_DATA_CONNECT')) { + curl_share_setopt($this->shareHandle, \CURLOPT_SHARE, \CURL_LOCK_DATA_CONNECT); + } + if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { + curl_share_setopt($this->shareHandle, \CURLOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION); + } + } catch (\Throwable $throwable) { + // don't crash if the share handle cannot be created + } + } } public function sendRequest(Request $request, Options $options): Response @@ -72,6 +106,9 @@ public function sendRequest(Request $request, Options $options): Response curl_setopt($curlHandle, \CURLOPT_RETURNTRANSFER, true); curl_setopt($curlHandle, \CURLOPT_HEADERFUNCTION, $responseHeaderCallback); curl_setopt($curlHandle, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1); + if ($this->shareHandle !== null) { + curl_setopt($curlHandle, \CURLOPT_SHARE, $this->shareHandle); + } $httpSslVerifyPeer = $options->getHttpSslVerifyPeer(); if (!$httpSslVerifyPeer) { From c563cef79b64269d1232f685ca1884d53c8b1c36 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 13 Jan 2026 15:48:21 +0100 Subject: [PATCH 2/9] use proper option --- src/HttpClient/HttpClient.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index 33d4d5d07..89bfa3ce4 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -49,12 +49,12 @@ public function __construct(string $sdkIdentifier, string $sdkVersion) if ($this->shareHandle === null) { try { $this->shareHandle = curl_share_init(); - curl_share_setopt($this->shareHandle, \CURLOPT_SHARE, \CURL_LOCK_DATA_DNS); + curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS); if (\defined('CURL_LOCK_DATA_CONNECT')) { - curl_share_setopt($this->shareHandle, \CURLOPT_SHARE, \CURL_LOCK_DATA_CONNECT); + curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT); } if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { - curl_share_setopt($this->shareHandle, \CURLOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION); + curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION); } } catch (\Throwable $throwable) { // don't crash if the share handle cannot be created From 224bd159242aeb7c6bb35301e1cd009ae437d76a Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 13 Jan 2026 16:02:14 +0100 Subject: [PATCH 3/9] add guard for constant --- src/HttpClient/HttpClient.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index 89bfa3ce4..10754eff4 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -34,7 +34,10 @@ public function __construct(string $sdkIdentifier, string $sdkVersion) $this->sdkIdentifier = $sdkIdentifier; $this->sdkVersion = $sdkVersion; if (\function_exists('curl_share_init_persistent')) { - $shareOptions = [\CURL_LOCK_DATA_DNS, \CURL_LOCK_DATA_CONNECT]; + $shareOptions = [\CURL_LOCK_DATA_DNS]; + if (\defined('CURL_LOCK_DATA_CONNECT')) { + $shareOptions[] = \CURL_LOCK_DATA_CONNECT; + } if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { $shareOptions[] = \CURL_LOCK_DATA_SSL_SESSION; } From 1724accea295ef647b234ed0c63d516bd684c830 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 16 Feb 2026 11:42:36 +0100 Subject: [PATCH 4/9] add flag --- src/HttpClient/HttpClient.php | 80 +++++++++++++++++++++-------------- src/Options.php | 28 ++++++++++++ src/functions.php | 1 + tests/OptionsTest.php | 7 +++ 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index 10754eff4..b81558d5c 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -33,36 +33,6 @@ public function __construct(string $sdkIdentifier, string $sdkVersion) { $this->sdkIdentifier = $sdkIdentifier; $this->sdkVersion = $sdkVersion; - if (\function_exists('curl_share_init_persistent')) { - $shareOptions = [\CURL_LOCK_DATA_DNS]; - if (\defined('CURL_LOCK_DATA_CONNECT')) { - $shareOptions[] = \CURL_LOCK_DATA_CONNECT; - } - if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { - $shareOptions[] = \CURL_LOCK_DATA_SSL_SESSION; - } - try { - $this->shareHandle = curl_share_init_persistent($shareOptions); - } catch (\Throwable $throwable) { - // don't crash if the share handle cannot be created - } - } - - // If the persistent share handle cannot be created or doesn't exist - if ($this->shareHandle === null) { - try { - $this->shareHandle = curl_share_init(); - curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS); - if (\defined('CURL_LOCK_DATA_CONNECT')) { - curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT); - } - if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { - curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION); - } - } catch (\Throwable $throwable) { - // don't crash if the share handle cannot be created - } - } } public function sendRequest(Request $request, Options $options): Response @@ -109,8 +79,9 @@ public function sendRequest(Request $request, Options $options): Response curl_setopt($curlHandle, \CURLOPT_RETURNTRANSFER, true); curl_setopt($curlHandle, \CURLOPT_HEADERFUNCTION, $responseHeaderCallback); curl_setopt($curlHandle, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1); - if ($this->shareHandle !== null) { - curl_setopt($curlHandle, \CURLOPT_SHARE, $this->shareHandle); + if ($options->isShareHandleEnabled()) { + $shareHandle = $this->getShareHandle(); + curl_setopt($curlHandle, \CURLOPT_SHARE, $shareHandle); } $httpSslVerifyPeer = $options->getHttpSslVerifyPeer(); @@ -165,4 +136,49 @@ public function sendRequest(Request $request, Options $options): Response return new Response($statusCode, $responseHeaders, $error); } + + /** + * Initializes a share handle for CURL requests. If available, it will always try to use a persistent + * share handle first and fall back to a regular share handle in case it's unavailable. + * + * @return object|resource|null a share handle or null if none could be created + */ + private function getShareHandle() + { + if ($this->shareHandle !== null) { + return $this->shareHandle; + } + if (\function_exists('curl_share_init_persistent')) { + $shareOptions = [\CURL_LOCK_DATA_DNS]; + if (\defined('CURL_LOCK_DATA_CONNECT')) { + $shareOptions[] = \CURL_LOCK_DATA_CONNECT; + } + if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { + $shareOptions[] = \CURL_LOCK_DATA_SSL_SESSION; + } + try { + $this->shareHandle = curl_share_init_persistent($shareOptions); + } catch (\Throwable $throwable) { + // don't crash if the share handle cannot be created + } + } + + // If the persistent share handle cannot be created or doesn't exist + if ($this->shareHandle === null) { + try { + $this->shareHandle = curl_share_init(); + curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS); + if (\defined('CURL_LOCK_DATA_CONNECT')) { + curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT); + } + if (\defined('CURL_LOCK_DATA_SSL_SESSION')) { + curl_share_setopt($this->shareHandle, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION); + } + } catch (\Throwable $throwable) { + // don't crash if the share handle cannot be created + } + } + + return $this->shareHandle; + } } diff --git a/src/Options.php b/src/Options.php index 39c3d92e4..0dddd94c8 100644 --- a/src/Options.php +++ b/src/Options.php @@ -1142,6 +1142,32 @@ public function setEnableHttpCompression(bool $enabled): self return $this; } + /** + * Returns whether a shared curl handle should be used or not. + * + * For PHP 8.5 and above, this will use the persistent curl handle. For previous PHP versions, it will use the + * regular share handle. + */ + public function isShareHandleEnabled(): bool + { + return $this->options['http_enable_curl_share_handle']; + } + + /** + * Sets whether the persistent curl handle should be used or not. + * + * For PHP 8.5 and above, this will use the persistent curl handle. For previous PHP versions, it will use the + * regular share handle. + */ + public function setEnableShareHandle(bool $enabled): self + { + $options = array_merge($this->options, ['http_enable_curl_share_handle' => $enabled]); + + $this->options = $this->resolver->resolve($options); + + return $this; + } + /** * Gets whether the silenced errors should be captured or not. * @@ -1341,6 +1367,7 @@ private function configureOptions(OptionsResolver $resolver): void 'http_ssl_verify_peer' => true, 'http_ssl_native_ca' => false, 'http_compression' => true, + 'http_enable_curl_share_handle' => true, 'capture_silenced_errors' => false, 'max_request_body_size' => 'medium', 'class_serializers' => [], @@ -1392,6 +1419,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('http_ssl_verify_peer', 'bool'); $resolver->setAllowedTypes('http_ssl_native_ca', 'bool'); $resolver->setAllowedTypes('http_compression', 'bool'); + $resolver->setAllowedTypes('http_enable_curl_share_handle', 'bool'); $resolver->setAllowedTypes('capture_silenced_errors', 'bool'); $resolver->setAllowedTypes('max_request_body_size', 'string'); $resolver->setAllowedTypes('class_serializers', 'array'); diff --git a/src/functions.php b/src/functions.php index 2ff257151..bc259656a 100644 --- a/src/functions.php +++ b/src/functions.php @@ -41,6 +41,7 @@ * http_proxy_authentication?: string|null, * http_ssl_verify_peer?: bool, * http_timeout?: int|float, + * http_enable_curl_share_handle?: bool, * ignore_exceptions?: array, * ignore_transactions?: array, * in_app_exclude?: array, diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index d229dad8b..860b88d5a 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -415,6 +415,13 @@ static function (): void {}, 'setEnableHttpCompression', ]; + yield [ + 'http_enable_curl_share_handle', + false, + 'isShareHandleEnabled', + 'setEnableShareHandle', + ]; + yield [ 'capture_silenced_errors', true, From acea12bf709d5fd74f7b771e7d52060945dd2143 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 16 Feb 2026 12:16:22 +0100 Subject: [PATCH 5/9] add tests for handle --- tests/HttpClient/HttpClientTest.php | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/HttpClient/HttpClientTest.php b/tests/HttpClient/HttpClientTest.php index 583ba7eff..13d580a37 100644 --- a/tests/HttpClient/HttpClientTest.php +++ b/tests/HttpClient/HttpClientTest.php @@ -75,6 +75,59 @@ public function testClientMakesUncompressedRequestWhenCompressionDisabled(): voi $this->assertEquals(\strlen($request->getStringBody()), $serverOutput['headers']['Content-Length']); } + public function testClientMakesRequestWhenShareHandleDisabled(): void + { + $testServer = $this->startTestServer(); + + $options = new Options([ + 'dsn' => "http://publicKey@{$testServer}/200", + 'http_enable_curl_share_handle' => false, + ]); + + $request = new Request(); + $request->setStringBody('test'); + + $client = new HttpClient('sentry.php', 'testing'); + $response = $client->sendRequest($request, $options); + + $serverOutput = $this->stopTestServer(); + + $this->assertTrue($response->isSuccess()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals($response->getStatusCode(), $serverOutput['status']); + $this->assertEquals($request->getStringBody(), $serverOutput['body']); + $this->assertNull($this->getShareHandleFromClient($client)); + } + + public function testShareHandleIsInitializedOnlyOncePerHttpClientInstance(): void + { + $testServer = $this->startTestServer(); + + $options = new Options([ + 'dsn' => "http://publicKey@{$testServer}/200", + 'http_enable_curl_share_handle' => true, + ]); + + $request = new Request(); + $request->setStringBody('test'); + + $client = new HttpClient('sentry.php', 'testing'); + + $firstResponse = $client->sendRequest($request, $options); + $firstShareHandle = $this->getShareHandleFromClient($client); + + $secondResponse = $client->sendRequest($request, $options); + $secondShareHandle = $this->getShareHandleFromClient($client); + + $this->stopTestServer(); + + $this->assertTrue($firstResponse->isSuccess()); + $this->assertTrue($secondResponse->isSuccess()); + $this->assertNotNull($firstShareHandle); + $this->assertShareHandleHasExpectedType($firstShareHandle); + $this->assertSame($firstShareHandle, $secondShareHandle); + } + public function testClientReturnsBodyAsErrorOnNonSuccessStatusCode(): void { $testServer = $this->startTestServer(); @@ -118,4 +171,38 @@ public function testThrowsExceptionIfRequestDataIsEmpty(): void $client = new HttpClient('sentry.php', 'testing'); $client->sendRequest(new Request(), $options); } + + /** + * @return object|resource|null + */ + private function getShareHandleFromClient(HttpClient $client) + { + $reflectionProperty = new \ReflectionProperty(HttpClient::class, 'shareHandle'); + if (\PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } + + return $reflectionProperty->getValue($client); + } + + /** + * @param object|resource $shareHandle + */ + private function assertShareHandleHasExpectedType($shareHandle): void + { + if (\PHP_VERSION_ID < 80000) { + $this->assertTrue(\is_resource($shareHandle)); + + return; + } + + if (\PHP_VERSION_ID >= 80500) { + $this->assertTrue(\class_exists('CurlSharePersistentHandle')); + $this->assertInstanceOf(\CurlSharePersistentHandle::class, $shareHandle); + + return; + } + + $this->assertInstanceOf(\CurlShareHandle::class, $shareHandle); + } } From e14dabe4f1131fd955e922fc3b4cb6ee46bddc66 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 16 Feb 2026 12:17:08 +0100 Subject: [PATCH 6/9] CS --- tests/HttpClient/HttpClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HttpClient/HttpClientTest.php b/tests/HttpClient/HttpClientTest.php index 13d580a37..45dd466e8 100644 --- a/tests/HttpClient/HttpClientTest.php +++ b/tests/HttpClient/HttpClientTest.php @@ -197,7 +197,7 @@ private function assertShareHandleHasExpectedType($shareHandle): void } if (\PHP_VERSION_ID >= 80500) { - $this->assertTrue(\class_exists('CurlSharePersistentHandle')); + $this->assertTrue(class_exists('CurlSharePersistentHandle')); $this->assertInstanceOf(\CurlSharePersistentHandle::class, $shareHandle); return; From e0dffc833e30b7f83b492d97e408c02a5f4a490f Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 16 Feb 2026 12:57:56 +0100 Subject: [PATCH 7/9] check share handle for null --- src/HttpClient/HttpClient.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index 692f8ac06..acb963e49 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -81,7 +81,9 @@ public function sendRequest(Request $request, Options $options): Response curl_setopt($curlHandle, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1); if ($options->isShareHandleEnabled()) { $shareHandle = $this->getShareHandle(); - curl_setopt($curlHandle, \CURLOPT_SHARE, $shareHandle); + if ($shareHandle !== null) { + curl_setopt($curlHandle, \CURLOPT_SHARE, $shareHandle); + } } $httpSslVerifyPeer = $options->getHttpSslVerifyPeer(); From cb198d2a4bd5cd7d3688fd588319bd260386501a Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 16 Feb 2026 13:00:25 +0100 Subject: [PATCH 8/9] lint --- src/Options.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Options.php b/src/Options.php index 0dddd94c8..3f75ad897 100644 --- a/src/Options.php +++ b/src/Options.php @@ -1150,7 +1150,12 @@ public function setEnableHttpCompression(bool $enabled): self */ public function isShareHandleEnabled(): bool { - return $this->options['http_enable_curl_share_handle']; + /** + * @var bool $shareHandleEnabled + */ + $shareHandleEnabled = $this->options['http_enable_curl_share_handle']; + + return $shareHandleEnabled; } /** From b8b385f28d9d6a8f94150dd505a6ee277dd9e710 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 16 Feb 2026 13:14:26 +0100 Subject: [PATCH 9/9] reset share handle on exception --- src/HttpClient/HttpClient.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index acb963e49..52006ee31 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -162,6 +162,7 @@ private function getShareHandle() $this->shareHandle = curl_share_init_persistent($shareOptions); } catch (\Throwable $throwable) { // don't crash if the share handle cannot be created + $this->shareHandle = null; } } @@ -178,6 +179,7 @@ private function getShareHandle() } } catch (\Throwable $throwable) { // don't crash if the share handle cannot be created + $this->shareHandle = null; } }