diff --git a/Core/src/RequestWrapper.php b/Core/src/RequestWrapper.php index e0d4d729d86..3ba7ead2817 100644 --- a/Core/src/RequestWrapper.php +++ b/Core/src/RequestWrapper.php @@ -465,12 +465,26 @@ private function getExceptionMessage(\Exception $ex) /** * Gets a set of request options. * + * Per-call options are merged over the client-level ones rather than replacing + * them, so that transport settings configured once on the client (`proxy`, + * `verify`, `cert`, ...) still apply to calls that supply their own options. + * The merge is shallow, because Guzzle options are a flat list of mostly + * scalar values; `headers` is the one nested array worth merging on its own, + * so that a per-call header does not drop the client's default headers. + * * @param array $options * @return array */ private function getRequestOptions(array $options) { - $restOptions = $options['restOptions'] ?? $this->restOptions; + $perCallRestOptions = $options['restOptions'] ?? []; + $headers = ($perCallRestOptions['headers'] ?? []) + ($this->restOptions['headers'] ?? []); + $restOptions = $perCallRestOptions + $this->restOptions; + + if ($headers) { + $restOptions['headers'] = $headers; + } + $timeout = $options['requestTimeout'] ?? $this->requestTimeout; if ($timeout && !array_key_exists('timeout', $restOptions)) { diff --git a/Core/tests/Unit/RequestWrapperTest.php b/Core/tests/Unit/RequestWrapperTest.php index e7b891e97f2..fa302411d02 100644 --- a/Core/tests/Unit/RequestWrapperTest.php +++ b/Core/tests/Unit/RequestWrapperTest.php @@ -106,6 +106,73 @@ public function testSuccessfullySendsAsyncRequest() ); } + public function testClientLevelRestOptionsSurvivePerCallRestOptions() + { + $actualOptions = []; + $requestWrapper = new RequestWrapper([ + 'accessToken' => 'abc', + 'restOptions' => ['proxy' => 'http://proxy.example.com:8080', 'verify' => false], + 'httpHandler' => function ($request, $options = []) use (&$actualOptions) { + $actualOptions = $options; + return new Response(200); + } + ]); + + $requestWrapper->send( + new Request('GET', 'http://www.example.com'), + ['restOptions' => ['headers' => ['X-Goog-Hash' => 'crc32c=abc']]] + ); + + $this->assertEquals('http://proxy.example.com:8080', $actualOptions['proxy']); + $this->assertFalse($actualOptions['verify']); + $this->assertEquals(['X-Goog-Hash' => 'crc32c=abc'], $actualOptions['headers']); + } + + public function testPerCallRestOptionsTakePrecedenceOverClientLevelOnes() + { + $actualOptions = []; + $requestWrapper = new RequestWrapper([ + 'accessToken' => 'abc', + 'restOptions' => ['proxy' => 'http://proxy.example.com:8080', 'debug' => false], + 'httpHandler' => function ($request, $options = []) use (&$actualOptions) { + $actualOptions = $options; + return new Response(200); + } + ]); + + $requestWrapper->send( + new Request('GET', 'http://www.example.com'), + ['restOptions' => ['debug' => true]] + ); + + $this->assertTrue($actualOptions['debug']); + $this->assertEquals('http://proxy.example.com:8080', $actualOptions['proxy']); + } + + public function testRestOptionHeadersAreMergedRatherThanReplaced() + { + $actualOptions = []; + $requestWrapper = new RequestWrapper([ + 'accessToken' => 'abc', + 'restOptions' => ['headers' => ['X-Client' => 'default', 'X-Keep' => 'yes']], + 'httpHandler' => function ($request, $options = []) use (&$actualOptions) { + $actualOptions = $options; + return new Response(200); + } + ]); + + $requestWrapper->send( + new Request('GET', 'http://www.example.com'), + ['restOptions' => ['headers' => ['X-Client' => 'per-call', 'X-Goog-Hash' => 'crc32c=abc']]] + ); + + $this->assertEquals([ + 'X-Client' => 'per-call', + 'X-Goog-Hash' => 'crc32c=abc', + 'X-Keep' => 'yes', + ], $actualOptions['headers']); + } + public function testSendAsyncRetriesOnFailure() { $actualDelays = 0;