diff --git a/lib/HttpClient.php b/lib/HttpClient.php index d2b5090..dda3208 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, @@ -190,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/lib/WorkOS.php b/lib/WorkOS.php index f849f84..ff7b75a 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 aabb144..b4f5f48 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 testPerRequestExtraHeadersCannotOverrideUserAgent(): 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( + 'WorkOS PHP Laravel/5.1.0', + $this->getLastRequest()->getHeaderLine('User-Agent'), + ); + } + public function testRateLimitErrorsExposeRetryAfter(): void { $client = $this->createMockClient([ diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 7b5a724..bd72a33 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, ); }