diff --git a/README.md b/README.md index d3a93ceb..5e365f7e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ multiple concurrent HTTP requests without blocking. * [withBase()](#withbase) * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) + * [withHeader()](#withheader) + * [withoutHeader()](#withoutheader) * [React\Http\Message](#reacthttpmessage) * [Response](#response) * [html()](#html) @@ -2381,6 +2383,37 @@ Notice that the [`Browser`](#browser) is an immutable object, i.e. this method actually returns a *new* [`Browser`](#browser) instance with the given setting applied. +#### withHeader() + +The `withHeader(string $header, string $value): Browser` method can be used to +add a request header for all following requests. +```php + var_dump($response); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Note that the new header will overwrite any headers previously set with +the same name (case-insensitive). Following requests will use these headers +by default unless they are explicitly set for any requests. + +#### withoutHeader() + +The `withoutHeader(string $header): Browser` method can be used to +remove any default request headers previously set via +the [`withHeader()` method](#withheader). + +```php + var_dump($response); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Note that this method only affects the headers which were set with the +method `withHeader(string $header, string $value): Browser` + ### React\Http\Message #### Response diff --git a/src/Browser.php b/src/Browser.php index 72847f66..f83e3883 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -23,6 +23,9 @@ class Browser private $transaction; private $baseUrl; private $protocolVersion = '1.1'; + private $defaultHeaders = array( + 'User-Agent' => 'ReactPHP/1', + ); /** * The `Browser` is responsible for sending HTTP requests to your HTTP server @@ -725,6 +728,64 @@ public function withResponseBuffer($maximumSize) )); } + /** + * Add a request header for all following requests. + * + * ```php + * var_dump($response); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * Note that the new header will overwrite any headers previously set with + * the same name (case-insensitive). Following requests will use these headers + * by default unless they are explicitly set for any requests. + * + * @param string $header + * @param string $value + * @return Browser + */ + public function withHeader($header, $value) + { + $browser = $this->withoutHeader($header); + $browser->defaultHeaders[$header] = $value; + + return $browser; + } + + /** + * Remove any default request headers previously set via + * the [`withHeader()` method](#withheader). + * + * ```php + * var_dump($response); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * Note that this method only affects the headers which were set with the + * method `withHeader(string $header, string $value): Browser` + * + * @param string $header + * @return Browser + */ + public function withoutHeader($header) + { + $browser = clone $this; + + /** @var string|int $key */ + foreach (\array_keys($browser->defaultHeaders) as $key) { + if (\strcasecmp($key, $header) === 0) { + unset($browser->defaultHeaders[$key]); + break; + } + } + + return $browser; + } + /** * Changes the [options](#options) to use: * @@ -783,6 +844,23 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), $body = new ReadableBodyStream($body); } + foreach ($this->defaultHeaders as $key => $value) { + if ($headers === array()) { + $headers = $this->defaultHeaders; + break; + } + + $explicitHeaderExists = false; + foreach (\array_keys($headers) as $headerKey) { + if (\strcasecmp($headerKey, $key) === 0) { + $explicitHeaderExists = true; + } + } + if (!$explicitHeaderExists) { + $headers[$key] = $value; + } + } + return $this->transaction->send( new Request($method, $url, $headers, $body, $this->protocolVersion) ); diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php index a5908a08..04bb4cad 100644 --- a/src/Client/RequestData.php +++ b/src/Client/RequestData.php @@ -29,7 +29,6 @@ private function mergeDefaultheaders(array $headers) $defaults = array_merge( array( 'Host' => $this->getHost().$port, - 'User-Agent' => 'ReactPHP/1', ), $connectionHeaders, $authHeaders diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 39be453a..6000f669 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -503,4 +503,114 @@ public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() $promise = $this->browser->get('http://example.com/'); $promise->cancel(); } + + public function testWithElseHeader() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('ACMC'), $request->getHeader('User-Agent')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithHeaderShouldOverwriteExistingHeader() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); //should be overwritten + $this->browser = $this->browser->withHeader('user-agent', 'ABC'); //should be the user-agent + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithHeadersShouldBeMergedCorrectlyWithDefaultHeaders() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/', array('user-Agent' => 'ABC')); //should win + } + + public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaultHeaders() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); + $this->browser = $this->browser->withHeader('User-Test', 'Test'); + $this->browser = $this->browser->withHeader('Custom-HEADER', 'custom'); + $this->browser = $this->browser->withHeader('just-a-header', 'header-value'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $expectedHeaders = array( + 'Host' => array('example.com'), + + 'User-Test' => array('Test'), + 'just-a-header' => array('header-value'), + + 'user-Agent' => array('ABC'), + 'another-header' => array('value'), + 'custom-header' => array('data'), + ); + + $that->assertEquals($expectedHeaders, $request->getHeaders()); + return true; + }))->willReturn(new Promise(function () { })); + + $headers = array( + 'user-Agent' => 'ABC', //should overwrite: 'User-Agent', 'ACMC' + 'another-header' => 'value', + 'custom-header' => 'data', //should overwrite: 'Custom-header', 'custom' + ); + $this->browser->get('http://example.com/', $headers); + } + + public function testWithoutHeaderShouldRemoveExistingHeader() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); + $this->browser = $this->browser->withoutHeader('UsEr-AgEnT'); //should remove case-insensitive header + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array(), $request->getHeader('user-agent')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testBrowserShouldHaveDefaultHeaderReactPHP() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array(0 => 'ReactPHP/1'), $request->getHeader('user-agent')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithoutHeaderShouldRemoveDefaultHeader() + { + $this->browser = $this->browser->withoutHeader('UsEr-AgEnT'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array(), $request->getHeader('User-Agent')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } } diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php index 7f96e152..f6713e85 100644 --- a/tests/Client/RequestDataTest.php +++ b/tests/Client/RequestDataTest.php @@ -14,7 +14,6 @@ public function toStringReturnsHTTPRequestMessage() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -27,7 +26,6 @@ public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() $expected = "GET /path?hello=world HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -40,7 +38,6 @@ public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath( $expected = "GET /?0 HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -53,7 +50,6 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm( $expected = "OPTIONS / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -66,7 +62,6 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm( $expected = "OPTIONS * HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -80,7 +75,6 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersion() $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -131,7 +125,6 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConst $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -145,7 +138,6 @@ public function toStringUsesUserPassFromURL() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "Authorization: Basic am9objpkdW1teQ==\r\n" . "\r\n"; diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index fb2dc884..cdb209cf 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -181,7 +181,7 @@ public function postRequestShouldSendAPostRequest() $this->stream ->expects($this->once()) ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome post data$#")); + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); $request->end('some post data'); @@ -199,7 +199,7 @@ public function writeWithAPostRequestShouldSendToTheStream() $this->successfulConnectionMock(); $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) ); @@ -222,7 +222,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $resolveConnection = $this->successfulAsyncConnectionMock(); $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( true @@ -258,7 +258,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $resolveConnection = $this->successfulAsyncConnectionMock(); $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( false @@ -290,7 +290,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() $this->successfulConnectionMock(); $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) );