From 179037035bdc65f7e031f5af86d1bbb4f3d3cddc Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 13 Oct 2026 16:40:02 -0500 Subject: [PATCH 1/5] feat: Add http_proxy option to route SDK requests through a web proxy --- .../Impl/Integrations/CurlEventPublisher.php | 8 +++ .../Integrations/GuzzleEventPublisher.php | 3 + .../Integrations/GuzzleFeatureRequester.php | 3 + src/LaunchDarkly/Integrations/Curl.php | 3 + src/LaunchDarkly/Integrations/Guzzle.php | 2 + src/LaunchDarkly/LDClient.php | 3 + test-service/SdkClientEntity.php | 1 + test-service/TestService.php | 3 +- .../Integrations/CurlEventPublisherTest.php | 18 ++++++ .../Impl/Integrations/EventPublisherTest.php | 57 +++++++++++++++++++ .../GuzzleFeatureRequesterTest.php | 43 ++++++++++++++ 11 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php index 75f951491..5572e6c0c 100644 --- a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php +++ b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php @@ -24,6 +24,7 @@ class CurlEventPublisher implements EventPublisher private int $_connectTimeout; private int $_timeout; private bool $_isWindows; + private ?string $_httpProxy; /** * The directory named by the payload_temp_dir option, or null if the option is not set. @@ -59,6 +60,7 @@ public function __construct(string $sdkKey, array $options = []) $this->_connectTimeout = intval($options['connect_timeout']); $this->_timeout = intval($options['timeout']); $this->_isWindows = PHP_OS_FAMILY == 'Windows'; + $this->_httpProxy = $options['http_proxy'] ?? null; $this->_payloadTempDir = $this->resolvePayloadTempDir($options['payload_temp_dir'] ?? null); } @@ -138,6 +140,9 @@ private function makeCurlRequest(string $payload, ?string $payloadFile = null): $args = " -X POST"; $args.= " --connect-timeout " . $this->_connectTimeout; $args.= " --max-time " . $this->_timeout; + if ($this->_httpProxy !== null) { + $args.= " --proxy " . escapeshellarg($this->_httpProxy); + } foreach ($this->_eventHeaders as $key => $value) { $args.= " -H " . escapeshellarg("$key: $value"); @@ -183,6 +188,9 @@ private function makePowershellRequest(string $payloadFile): bool $args.= " -UseBasicParsing"; $args.= " -InFile '$payloadFile'"; $args.= " -H @{" . $headerString . "}"; + if ($this->_httpProxy !== null) { + $args.= " -Proxy " . escapeshellarg($this->_httpProxy); + } $args.= " -Uri " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); $args.= " ; Remove-Item '$payloadFile'"; diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php index fb07b5818..3b2ec9ab3 100644 --- a/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php @@ -38,6 +38,9 @@ public function __construct(string $sdkKey, array $options = []) 'timeout' => $options['timeout'], 'connect_timeout' => $options['connect_timeout'] ]; + if (isset($options['http_proxy'])) { + $this->_requestOptions['proxy'] = $options['http_proxy']; + } } public function publish(string $payload): bool diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php index e012d582a..d643cb4a9 100644 --- a/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php @@ -62,6 +62,9 @@ public function __construct(string $baseUri, string $sdkKey, array $options) 'debug' => $options['debug'] ?? false, 'base_uri' => $baseUri ]; + if (isset($options['http_proxy'])) { + $defaults['proxy'] = $options['http_proxy']; + } $this->_client = new Client($defaults); } diff --git a/src/LaunchDarkly/Integrations/Curl.php b/src/LaunchDarkly/Integrations/Curl.php index 3d81480b6..0d6a91293 100644 --- a/src/LaunchDarkly/Integrations/Curl.php +++ b/src/LaunchDarkly/Integrations/Curl.php @@ -34,6 +34,9 @@ class Curl * with `Argument exceeds the allowed length of N bytes`. On Windows the payload always goes into a * file, and this option only names the directory; the default there is the system temporary * directory. + * - `http_proxy`: URL of a web proxy for the requests; see {@see \LaunchDarkly\LDClient::__construct()}. + * On Windows, PowerShell does not read credentials from the proxy URL. Use a proxy that does not + * require authentication there. * @return mixed an object to be stored in the `event_publisher` configuration property */ public static function eventPublisher(array $options = []): mixed diff --git a/src/LaunchDarkly/Integrations/Guzzle.php b/src/LaunchDarkly/Integrations/Guzzle.php index ba712832b..0f8f229c2 100644 --- a/src/LaunchDarkly/Integrations/Guzzle.php +++ b/src/LaunchDarkly/Integrations/Guzzle.php @@ -22,6 +22,7 @@ class Guzzle * - `cache`: an optional object that implements `Kevinrob\GuzzleCache\Storage\CacheStorageInterface` * - `connect_timeout`: connection timeout in seconds; defaults to 3 * - `timeout`: read timeout in seconds; defaults to 3 + * - `http_proxy`: URL of a web proxy for the requests; see {@see \LaunchDarkly\LDClient::__construct()} * @return mixed an object to be stored in the `feature_requester` configuration property */ public static function featureRequester(array $options = []): mixed @@ -54,6 +55,7 @@ public static function featureRequester(array $options = []): mixed * - `events_uri`: URI of the server that will receive events, if it is `ld-relay` instead of LaunchDarkly * - `connect_timeout`: connection timeout in seconds; defaults to 3 * - `timeout`: read timeout in seconds; defaults to 3 + * - `http_proxy`: URL of a web proxy for the requests; see {@see \LaunchDarkly\LDClient::__construct()} * @return mixed an object to be stored in the `event_publisher` configuration property */ public static function eventPublisher(array $options = []): mixed diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index d545cebba..afc558056 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -74,6 +74,9 @@ class LDClient * - `events_uri`: Base URI for sending events to LaunchDarkly. Change this if you are forwarding events through a Relay Proxy instance. * - `timeout`: The maximum length of an HTTP request in seconds. Defaults to 3. * - `connect_timeout`: The maximum number of seconds to wait while trying to connect to a server. Defaults to 3. + * - `http_proxy`: The URL of a web proxy for all connections to LaunchDarkly, for example `http://my-proxy:8080` + * or `http://user:pass@my-proxy:8080`. This value has priority over the proxy environment variables, such as + * `HTTPS_PROXY`, for SDK connections only. If you do not set it, the SDK uses those environment variables. * - `cache`: An optional implementation of Guzzle's [CacheStorageInterface](https://github.com/Kevinrob/guzzle-cache-middleware/blob/master/src/Storage/CacheStorageInterface.php). * Defaults to an in-memory cache. * - `send_events`: If set to false, disables the sending of events to LaunchDarkly. Defaults to true. diff --git a/test-service/SdkClientEntity.php b/test-service/SdkClientEntity.php index 0b4f04ada..8bd47ee6a 100644 --- a/test-service/SdkClientEntity.php +++ b/test-service/SdkClientEntity.php @@ -52,6 +52,7 @@ public static function createSdkClient($params, bool $resetBigSegmentsStore, $lo $pollingConfig = $config['polling'] ?? []; $options['base_uri'] = $pollingConfig['baseUri'] ?? null; + $options['http_proxy'] = $config['proxy']['httpProxy'] ?? null; $options['send_events'] = ($config['events'] ?? null) !== null; $eventsConfig = $config['events'] ?? []; diff --git a/test-service/TestService.php b/test-service/TestService.php index 267e3a907..0d12967a8 100644 --- a/test-service/TestService.php +++ b/test-service/TestService.php @@ -83,7 +83,8 @@ public function getStatus(): array 'big-segments', 'evaluation-hooks', 'hook-environment-id', - 'track-hooks' + 'track-hooks', + 'http-proxy' ], 'clientVersion' => \LaunchDarkly\LDClient::VERSION ]; diff --git a/tests/Impl/Integrations/CurlEventPublisherTest.php b/tests/Impl/Integrations/CurlEventPublisherTest.php index 1410ad234..df30d977b 100644 --- a/tests/Impl/Integrations/CurlEventPublisherTest.php +++ b/tests/Impl/Integrations/CurlEventPublisherTest.php @@ -159,4 +159,22 @@ public function testRemovesPayloadFileAfterTheRequest() $this->assertEmpty(glob($dir . '/ld-*') ?: []); } + + public function testPassesHttpProxyToCurl() + { + $publisher = $this->makePublisher(['http_proxy' => 'http://user:pass@my-proxy:8080']); + $this->assertTrue($publisher->publish(self::PAYLOAD)); + + $args = $this->awaitRecordedArgs(); + $index = array_search('--proxy', $args, true); + $this->assertNotFalse($index); + $this->assertEquals('http://user:pass@my-proxy:8080', $args[$index + 1]); + } + + public function testDoesNotPassProxyByDefault() + { + $this->assertTrue($this->makePublisher()->publish(self::PAYLOAD)); + + $this->assertNotContains('--proxy', $this->awaitRecordedArgs()); + } } diff --git a/tests/Impl/Integrations/EventPublisherTest.php b/tests/Impl/Integrations/EventPublisherTest.php index b04b1428e..5de10e49e 100644 --- a/tests/Impl/Integrations/EventPublisherTest.php +++ b/tests/Impl/Integrations/EventPublisherTest.php @@ -51,6 +51,63 @@ public function getEventPublisher(): array ]; } + public function getProxiedEventPublisher(): array + { + /** @var LoggerInterface **/ + $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + + // The host does not resolve, so the request can only arrive through the proxy. + $config = [ + 'events_uri' => 'http://not.valid.local', + 'timeout' => 3, + 'connect_timeout' => 3, + 'logger' => $logger, + 'http_proxy' => 'http://localhost:8080', + ]; + + return [ + [new Integrations\CurlEventPublisher('sdk-key', $config)], + [new Integrations\CurlEventPublisher('sdk-key', array_merge($config, ['payload_temp_dir' => true]))], + [new Integrations\GuzzleEventPublisher('sdk-key', $config)], + ]; + } + + /** + * @dataProvider getProxiedEventPublisher + */ + public function testSendsThroughHttpProxy($publisher) + { + $event = json_encode(["key" => "user-key"]); + $publisher->publish($event); + + $requests = []; + $client = new Client(); + + // Provide time for the curl to execute + $start = time(); + while (time() - $start < 5) { + $response = $client->request('GET', 'http://localhost:8080/__admin/requests'); + $body = json_decode($response->getBody()->getContents(), true); + $requests = $body['requests']; + + if ($requests) { + break; + } + usleep(100); + } + + if (!$requests) { + $this->fail("Unable to connect to endpoint within specified timeout"); + } + + $this->assertCount(1, $requests); + + $request = $requests[0]['request']; + $this->assertEquals($event, $request['body']); + $this->assertEquals('/bulk', $request['url']); + $this->assertEquals('not.valid.local', $request['headers']['Host']); + } + /** * @dataProvider getEventPublisher */ diff --git a/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php b/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php index da98eee39..682f1c58d 100644 --- a/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php +++ b/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php @@ -77,6 +77,49 @@ public function testSendsCorrectHeaders(): void $this->assertEquals('my-instance-id', $headers['X-LaunchDarkly-Instance-Id']); } + public function testSendsRequestThroughHttpProxy(): void + { + /** @var LoggerInterface **/ + $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + + $config = [ + 'logger' => $logger, + 'timeout' => 3, + 'connect_timeout' => 3, + 'http_proxy' => 'http://localhost:8080', + ]; + + // The host does not resolve, so the request can only arrive through the proxy. + $requester = new GuzzleFeatureRequester('http://not.valid.local', 'sdk-key', $config); + $requester->getFeature("flag-key"); + + $requests = []; + $client = new Client(); + + // Provide time for the curl to execute + $start = time(); + while (time() - $start < 5) { + $response = $client->request('GET', 'http://localhost:8080/__admin/requests'); + $body = json_decode($response->getBody()->getContents(), true); + $requests = $body['requests']; + + if ($requests) { + break; + } + usleep(100); + } + + if (!$requests) { + $this->fail("Unable to connect to endpoint within specified timeout"); + } + + $this->assertCount(1, $requests); + + $request = $requests[0]['request']; + $this->assertEquals('/sdk/flags/flag-key', $request['url']); + $this->assertEquals('not.valid.local', $request['headers']['Host']); + } + public function wrapperProvider(): array { return [ From 08039fadad70a9bca4dfe354c7602be300494d61 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 13 Oct 2026 17:17:17 -0500 Subject: [PATCH 2/5] fix: Quote PowerShell event publisher arguments for PowerShell --- .../Impl/Integrations/CurlEventPublisher.php | 10 ++++++---- src/LaunchDarkly/LDClient.php | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php index 5572e6c0c..0e8deb348 100644 --- a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php +++ b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php @@ -183,16 +183,18 @@ private function makePowershellRequest(string $payloadFile): bool } $scheme = $this->_ssl ? "https://" : "http://"; + $escapedFile = str_replace("'", "''", $payloadFile); + $escapedUri = str_replace("'", "''", $scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); $args = " Invoke-WebRequest"; $args.= " -Method POST"; $args.= " -UseBasicParsing"; - $args.= " -InFile '$payloadFile'"; + $args.= " -InFile '$escapedFile'"; $args.= " -H @{" . $headerString . "}"; if ($this->_httpProxy !== null) { - $args.= " -Proxy " . escapeshellarg($this->_httpProxy); + $args.= " -Proxy '" . str_replace("'", "''", $this->_httpProxy) . "'"; } - $args.= " -Uri " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); - $args.= " ; Remove-Item '$payloadFile'"; + $args.= " -Uri '$escapedUri'"; + $args.= " ; Remove-Item '$escapedFile'"; $cmd = base64_encode(iconv('ISO-8859-1', 'UTF-16LE', $args)); shell_exec("start /B powershell.exe -encodedCommand $cmd > nul 2>&1"); diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index afc558056..9243d23dc 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -76,7 +76,8 @@ class LDClient * - `connect_timeout`: The maximum number of seconds to wait while trying to connect to a server. Defaults to 3. * - `http_proxy`: The URL of a web proxy for all connections to LaunchDarkly, for example `http://my-proxy:8080` * or `http://user:pass@my-proxy:8080`. This value has priority over the proxy environment variables, such as - * `HTTPS_PROXY`, for SDK connections only. If you do not set it, the SDK uses those environment variables. + * `HTTPS_PROXY`, for SDK connections only. If you do not set it, the SDK uses those environment variables. On + * Windows, the default event publisher uses PowerShell, which does not read credentials from the proxy URL. * - `cache`: An optional implementation of Guzzle's [CacheStorageInterface](https://github.com/Kevinrob/guzzle-cache-middleware/blob/master/src/Storage/CacheStorageInterface.php). * Defaults to an in-memory cache. * - `send_events`: If set to false, disables the sending of events to LaunchDarkly. Defaults to true. From c031006fc983fa3b4f96a3601f23fa381228b22f Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 13 Oct 2026 17:24:22 -0500 Subject: [PATCH 3/5] docs: Align http_proxy option docs with other SDKs --- src/LaunchDarkly/LDClient.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 9243d23dc..30c781cb2 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -74,10 +74,11 @@ class LDClient * - `events_uri`: Base URI for sending events to LaunchDarkly. Change this if you are forwarding events through a Relay Proxy instance. * - `timeout`: The maximum length of an HTTP request in seconds. Defaults to 3. * - `connect_timeout`: The maximum number of seconds to wait while trying to connect to a server. Defaults to 3. - * - `http_proxy`: The URL of a web proxy for all connections to LaunchDarkly, for example `http://my-proxy:8080` - * or `http://user:pass@my-proxy:8080`. This value has priority over the proxy environment variables, such as - * `HTTPS_PROXY`, for SDK connections only. If you do not set it, the SDK uses those environment variables. On - * Windows, the default event publisher uses PowerShell, which does not read credentials from the proxy URL. + * - `http_proxy`: Use a proxy when the SDK connects to LaunchDarkly. This is the full URL of the proxy, for example + * `http://my-proxy.com:1234` or `http://user:pass@my-proxy.com:1234`. The SDK uses the proxy for HTTP and HTTPS + * targets. This option overrides the `HTTPS_PROXY` and `HTTP_PROXY` environment variables, but only for SDK + * connections. The `NO_PROXY` environment variable still applies, because libcurl reads it. On Windows, the + * default event publisher uses PowerShell, which does not read credentials from the proxy URL. * - `cache`: An optional implementation of Guzzle's [CacheStorageInterface](https://github.com/Kevinrob/guzzle-cache-middleware/blob/master/src/Storage/CacheStorageInterface.php). * Defaults to an in-memory cache. * - `send_events`: If set to false, disables the sending of events to LaunchDarkly. Defaults to true. From 9732190c61ed60485c79cca931428c6509d26864 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 13 Oct 2026 17:29:00 -0500 Subject: [PATCH 4/5] fix: Ignore invalid http_proxy values instead of failing --- .../Impl/Integrations/CurlEventPublisher.php | 2 +- .../Integrations/GuzzleEventPublisher.php | 5 +++-- .../Integrations/GuzzleFeatureRequester.php | 5 +++-- src/LaunchDarkly/Impl/Util.php | 21 +++++++++++++++++++ test-service/SdkClientEntity.php | 1 - test-service/TestService.php | 3 +-- .../Integrations/CurlEventPublisherTest.php | 11 ++++++++++ 7 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php index 0e8deb348..7474006fa 100644 --- a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php +++ b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php @@ -60,7 +60,7 @@ public function __construct(string $sdkKey, array $options = []) $this->_connectTimeout = intval($options['connect_timeout']); $this->_timeout = intval($options['timeout']); $this->_isWindows = PHP_OS_FAMILY == 'Windows'; - $this->_httpProxy = $options['http_proxy'] ?? null; + $this->_httpProxy = Util::httpProxy($options); $this->_payloadTempDir = $this->resolvePayloadTempDir($options['payload_temp_dir'] ?? null); } diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php index 3b2ec9ab3..ee0283b8e 100644 --- a/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php @@ -38,8 +38,9 @@ public function __construct(string $sdkKey, array $options = []) 'timeout' => $options['timeout'], 'connect_timeout' => $options['connect_timeout'] ]; - if (isset($options['http_proxy'])) { - $this->_requestOptions['proxy'] = $options['http_proxy']; + $proxy = Util::httpProxy($options); + if ($proxy !== null) { + $this->_requestOptions['proxy'] = $proxy; } } diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php index d643cb4a9..5ddffa516 100644 --- a/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php @@ -62,8 +62,9 @@ public function __construct(string $baseUri, string $sdkKey, array $options) 'debug' => $options['debug'] ?? false, 'base_uri' => $baseUri ]; - if (isset($options['http_proxy'])) { - $defaults['proxy'] = $options['http_proxy']; + $proxy = Util::httpProxy($options); + if ($proxy !== null) { + $defaults['proxy'] = $proxy; } $this->_client = new Client($defaults); diff --git a/src/LaunchDarkly/Impl/Util.php b/src/LaunchDarkly/Impl/Util.php index 0cfcefa00..b0a3b0f8e 100644 --- a/src/LaunchDarkly/Impl/Util.php +++ b/src/LaunchDarkly/Impl/Util.php @@ -82,6 +82,27 @@ public static function logExceptionAtErrorLevel(LoggerInterface $logger, \Throwa ); } + /** + * Reads the http_proxy option. + * + * A value that is not a non-empty string is ignored, so the proxy environment variables apply. + * + * @return ?string the proxy URL, or null if there is no valid value + */ + public static function httpProxy(array $options): ?string + { + $proxy = $options['http_proxy'] ?? null; + if ($proxy === null || (is_string($proxy) && $proxy !== '')) { + return $proxy; + } + + $logger = $options['logger'] ?? null; + if ($logger instanceof LoggerInterface) { + $logger->warning('The http_proxy option is not a non-empty string. The SDK ignores it.'); + } + return null; + } + public static function makeNullLogger(): LoggerInterface { return new Logger('', [new NullHandler()]); diff --git a/test-service/SdkClientEntity.php b/test-service/SdkClientEntity.php index 8bd47ee6a..0b4f04ada 100644 --- a/test-service/SdkClientEntity.php +++ b/test-service/SdkClientEntity.php @@ -52,7 +52,6 @@ public static function createSdkClient($params, bool $resetBigSegmentsStore, $lo $pollingConfig = $config['polling'] ?? []; $options['base_uri'] = $pollingConfig['baseUri'] ?? null; - $options['http_proxy'] = $config['proxy']['httpProxy'] ?? null; $options['send_events'] = ($config['events'] ?? null) !== null; $eventsConfig = $config['events'] ?? []; diff --git a/test-service/TestService.php b/test-service/TestService.php index 0d12967a8..267e3a907 100644 --- a/test-service/TestService.php +++ b/test-service/TestService.php @@ -83,8 +83,7 @@ public function getStatus(): array 'big-segments', 'evaluation-hooks', 'hook-environment-id', - 'track-hooks', - 'http-proxy' + 'track-hooks' ], 'clientVersion' => \LaunchDarkly\LDClient::VERSION ]; diff --git a/tests/Impl/Integrations/CurlEventPublisherTest.php b/tests/Impl/Integrations/CurlEventPublisherTest.php index df30d977b..069f30ce9 100644 --- a/tests/Impl/Integrations/CurlEventPublisherTest.php +++ b/tests/Impl/Integrations/CurlEventPublisherTest.php @@ -177,4 +177,15 @@ public function testDoesNotPassProxyByDefault() $this->assertNotContains('--proxy', $this->awaitRecordedArgs()); } + + /** + * @testWith [false] + * [""] + */ + public function testIgnoresInvalidHttpProxy(mixed $proxy) + { + $this->assertTrue($this->makePublisher(['http_proxy' => $proxy])->publish(self::PAYLOAD)); + + $this->assertNotContains('--proxy', $this->awaitRecordedArgs()); + } } From 3f6f86294e48892278ffbccc1a0428366e41a567 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 13 Oct 2026 18:09:24 -0500 Subject: [PATCH 5/5] docs: Clarify that the curl event publisher uses PowerShell on Windows --- src/LaunchDarkly/Integrations/Curl.php | 7 +++++-- src/LaunchDarkly/LDClient.php | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly/Integrations/Curl.php b/src/LaunchDarkly/Integrations/Curl.php index 0d6a91293..d2784715b 100644 --- a/src/LaunchDarkly/Integrations/Curl.php +++ b/src/LaunchDarkly/Integrations/Curl.php @@ -25,6 +25,9 @@ class Curl * This implementation forks a process for each event payload. Alternatively, you can use * {@see \LaunchDarkly\Integrations\Guzzle::eventPublisher()}, which makes synchronous requests. * + * On Windows, this publisher does not use curl. It sends events with the PowerShell `Invoke-WebRequest` + * command, and it ignores the `curl` option. + * * @param array $options Configuration settings (can also be passed in the main client configuration): * - `curl`: command for executing `curl`; defaults to `/usr/bin/env curl` * - `payload_temp_dir`: directory for the temporary file that holds the event payload. Use `true` @@ -35,8 +38,8 @@ class Curl * file, and this option only names the directory; the default there is the system temporary * directory. * - `http_proxy`: URL of a web proxy for the requests; see {@see \LaunchDarkly\LDClient::__construct()}. - * On Windows, PowerShell does not read credentials from the proxy URL. Use a proxy that does not - * require authentication there. + * On Windows, `Invoke-WebRequest` does not read credentials from the proxy URL. To use a proxy that + * requires authentication on Windows, use {@see \LaunchDarkly\Integrations\Guzzle::eventPublisher()}. * @return mixed an object to be stored in the `event_publisher` configuration property */ public static function eventPublisher(array $options = []): mixed diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 30c781cb2..30ff149f9 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -78,7 +78,8 @@ class LDClient * `http://my-proxy.com:1234` or `http://user:pass@my-proxy.com:1234`. The SDK uses the proxy for HTTP and HTTPS * targets. This option overrides the `HTTPS_PROXY` and `HTTP_PROXY` environment variables, but only for SDK * connections. The `NO_PROXY` environment variable still applies, because libcurl reads it. On Windows, the - * default event publisher uses PowerShell, which does not read credentials from the proxy URL. + * default event publisher uses PowerShell, which does not read credentials from the proxy URL. See + * {@see \LaunchDarkly\Integrations\Curl::eventPublisher()}. * - `cache`: An optional implementation of Guzzle's [CacheStorageInterface](https://github.com/Kevinrob/guzzle-cache-middleware/blob/master/src/Storage/CacheStorageInterface.php). * Defaults to an in-memory cache. * - `send_events`: If set to false, disables the sending of events to LaunchDarkly. Defaults to true.