Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/HttpClient/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ 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;
Expand Down Expand Up @@ -72,6 +79,12 @@ 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 ($options->isShareHandleEnabled()) {
$shareHandle = $this->getShareHandle();
if ($shareHandle !== null) {
curl_setopt($curlHandle, \CURLOPT_SHARE, $shareHandle);
}
}

$httpSslVerifyPeer = $options->getHttpSslVerifyPeer();
if (!$httpSslVerifyPeer) {
Expand Down Expand Up @@ -125,4 +138,51 @@ 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
$this->shareHandle = null;
}
}

// 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
$this->shareHandle = null;
}
}

return $this->shareHandle;
}
}
33 changes: 33 additions & 0 deletions src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,37 @@ 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
{
/**
* @var bool $shareHandleEnabled
*/
$shareHandleEnabled = $this->options['http_enable_curl_share_handle'];

return $shareHandleEnabled;
}

/**
* 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.
*
Expand Down Expand Up @@ -1341,6 +1372,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' => [],
Expand Down Expand Up @@ -1392,6 +1424,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');
Expand Down
1 change: 1 addition & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* http_proxy_authentication?: string|null,
* http_ssl_verify_peer?: bool,
* http_timeout?: int|float,
* http_enable_curl_share_handle?: bool,
* ignore_exceptions?: array<class-string>,
* ignore_transactions?: array<string>,
* in_app_exclude?: array<string>,
Expand Down
87 changes: 87 additions & 0 deletions tests/HttpClient/HttpClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions tests/OptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,13 @@ static function (): void {},
'setEnableHttpCompression',
];

yield [
'http_enable_curl_share_handle',
false,
'isShareHandleEnabled',
'setEnableShareHandle',
];

yield [
'capture_silenced_errors',
true,
Expand Down