From 4bf6f1d9ebbf41ff124dabeac0096181d180628f Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 13 Apr 2026 14:02:43 -0400 Subject: [PATCH 1/2] feat: add default User-Agent header and optional override Every outbound request now sends a User-Agent identifying the SDK (e.g. "WorkOS PHP/5.0.0"), sourced from Version::SDK_IDENTIFIER / Version::SDK_VERSION. Wrappers like workos-php-laravel can supply their own identifier via a new optional userAgent parameter on the WorkOS constructor, and individual requests can still override via RequestOptions::extraHeaders. Precedence: per-request extraHeaders > per-client constructor arg > default from Version. Note: lib/WorkOS.php is oagen-generated; the oagen template needs the same constructor param + forwarding change so the next regeneration doesn't clobber this. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/HttpClient.php | 2 ++ lib/WorkOS.php | 3 +- tests/Service/RuntimeBehaviorTest.php | 48 +++++++++++++++++++++++++++ tests/TestHelper.php | 2 ++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/HttpClient.php b/lib/HttpClient.php index d2b50904..5cf0106c 100644 --- a/lib/HttpClient.php +++ b/lib/HttpClient.php @@ -37,6 +37,7 @@ public function __construct( private readonly int $timeout, private readonly int $maxRetries, ?\GuzzleHttp\HandlerStack $handler = null, + private readonly ?string $userAgent = null, ) { $this->client = new Client([ 'handler' => $handler, @@ -176,6 +177,7 @@ private function buildRequestOptions( ): array { $headers = [ 'Content-Type' => 'application/json', + 'User-Agent' => $this->userAgent ?? sprintf('%s/%s', Version::SDK_IDENTIFIER, Version::SDK_VERSION), ]; if ($this->getApiKey() !== null) { diff --git a/lib/WorkOS.php b/lib/WorkOS.php index f849f841..ff7b75af 100644 --- a/lib/WorkOS.php +++ b/lib/WorkOS.php @@ -80,10 +80,11 @@ public function __construct( int $timeout = 60, int $maxRetries = 3, ?\GuzzleHttp\HandlerStack $handler = null, + ?string $userAgent = null, ) { $apiKey ??= getenv('WORKOS_API_KEY') ?: self::$apiKey ?? ''; $clientId ??= getenv('WORKOS_CLIENT_ID') ?: self::$clientId; - $this->httpClient = new HttpClient($apiKey, $clientId, $baseUrl, $timeout, $maxRetries, $handler); + $this->httpClient = new HttpClient($apiKey, $clientId, $baseUrl, $timeout, $maxRetries, $handler, $userAgent); } public function apiKeys(): ApiKeys diff --git a/tests/Service/RuntimeBehaviorTest.php b/tests/Service/RuntimeBehaviorTest.php index aabb1442..ec081f71 100644 --- a/tests/Service/RuntimeBehaviorTest.php +++ b/tests/Service/RuntimeBehaviorTest.php @@ -9,6 +9,7 @@ use WorkOS\Exception\RateLimitExceededException; use WorkOS\RequestOptions; use WorkOS\TestHelper; +use WorkOS\Version; class RuntimeBehaviorTest extends TestCase { @@ -125,6 +126,53 @@ public function testAuthenticationErrorsAreMapped(): void } } + public function testDefaultUserAgentIdentifiesTheSdk(): void + { + $client = $this->createMockClient([ + ['status' => 200, 'body' => $this->loadFixture('organization')], + ]); + + $client->organizations()->getOrganization('org_123'); + + $this->assertSame( + sprintf('%s/%s', Version::SDK_IDENTIFIER, Version::SDK_VERSION), + $this->getLastRequest()->getHeaderLine('User-Agent'), + ); + } + + public function testConstructorUserAgentOverridesDefault(): void + { + $client = $this->createMockClient( + [['status' => 200, 'body' => $this->loadFixture('organization')]], + userAgent: 'WorkOS PHP Laravel/5.1.0', + ); + + $client->organizations()->getOrganization('org_123'); + + $this->assertSame( + 'WorkOS PHP Laravel/5.1.0', + $this->getLastRequest()->getHeaderLine('User-Agent'), + ); + } + + public function testPerRequestUserAgentBeatsConstructorUserAgent(): void + { + $client = $this->createMockClient( + [['status' => 200, 'body' => $this->loadFixture('organization')]], + userAgent: 'WorkOS PHP Laravel/5.1.0', + ); + + $client->organizations()->getOrganization( + 'org_123', + new RequestOptions(extraHeaders: ['User-Agent' => 'Custom/9.9.9']), + ); + + $this->assertSame( + 'Custom/9.9.9', + $this->getLastRequest()->getHeaderLine('User-Agent'), + ); + } + public function testRateLimitErrorsExposeRetryAfter(): void { $client = $this->createMockClient([ diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 7b5a7240..bd72a33d 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -30,6 +30,7 @@ protected function createMockClient( ?string $clientId = 'test_client_id', string $baseUrl = 'https://api.workos.com', int $maxRetries = 3, + ?string $userAgent = null, ): WorkOS { $mockResponses = array_map( fn (array $response) => new Response( @@ -51,6 +52,7 @@ protected function createMockClient( baseUrl: $baseUrl, maxRetries: $maxRetries, handler: $handler, + userAgent: $userAgent, ); } From 6f5c86c6b4d70bb7de8a27b6f020c8c40ffbb098 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 13 Apr 2026 14:28:53 -0400 Subject: [PATCH 2/2] fix: preserve UA string versioning --- lib/HttpClient.php | 4 +++- tests/Service/RuntimeBehaviorTest.php | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/HttpClient.php b/lib/HttpClient.php index 5cf0106c..dda3208d 100644 --- a/lib/HttpClient.php +++ b/lib/HttpClient.php @@ -177,7 +177,6 @@ private function buildRequestOptions( ): array { $headers = [ 'Content-Type' => 'application/json', - 'User-Agent' => $this->userAgent ?? sprintf('%s/%s', Version::SDK_IDENTIFIER, Version::SDK_VERSION), ]; if ($this->getApiKey() !== null) { @@ -192,6 +191,9 @@ private function buildRequestOptions( $headers['Idempotency-Key'] = $options->idempotencyKey; } + // Always set User-Agent last so callers cannot override it via extraHeaders. + $headers['User-Agent'] = $this->userAgent ?? sprintf('%s/%s', Version::SDK_IDENTIFIER, Version::SDK_VERSION); + $requestOptions = [ 'headers' => $headers, 'http_errors' => false, diff --git a/tests/Service/RuntimeBehaviorTest.php b/tests/Service/RuntimeBehaviorTest.php index ec081f71..b4f5f488 100644 --- a/tests/Service/RuntimeBehaviorTest.php +++ b/tests/Service/RuntimeBehaviorTest.php @@ -155,7 +155,7 @@ public function testConstructorUserAgentOverridesDefault(): void ); } - public function testPerRequestUserAgentBeatsConstructorUserAgent(): void + public function testPerRequestExtraHeadersCannotOverrideUserAgent(): void { $client = $this->createMockClient( [['status' => 200, 'body' => $this->loadFixture('organization')]], @@ -168,7 +168,7 @@ public function testPerRequestUserAgentBeatsConstructorUserAgent(): void ); $this->assertSame( - 'Custom/9.9.9', + 'WorkOS PHP Laravel/5.1.0', $this->getLastRequest()->getHeaderLine('User-Agent'), ); }