diff --git a/src/EventSource.php b/src/EventSource.php index 6ec3c88..fd7d38c 100644 --- a/src/EventSource.php +++ b/src/EventSource.php @@ -77,8 +77,13 @@ class EventSource extends EventEmitter private $request; private $timer; private $reconnectTime = 3.0; + private $headers; + private $defaultHeaders = [ + 'Accept' => 'text/event-stream', + 'Cache-Control' => 'no-cache' + ]; - public function __construct($url, LoopInterface $loop, Browser $browser = null) + public function __construct($url, LoopInterface $loop, Browser $browser = null, array $headers = []) { $parts = parse_url($url); if (!isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('http', 'https'))) { @@ -92,16 +97,35 @@ public function __construct($url, LoopInterface $loop, Browser $browser = null) $this->loop = $loop; $this->url = $url; + $this->headers = $this->mergeHeaders($headers); + $this->readyState = self::CONNECTING; $this->request(); } - private function request() + private function mergeHeaders(array $headers = []) { - $headers = array( - 'Accept' => 'text/event-stream', - 'Cache-Control' => 'no-cache' + if ($headers === []) { + return $this->defaultHeaders; + } + + // HTTP headers are case insensitive, we do not want to have different cases for the same (default) header + // Convert default headers to lowercase, to ease the custom headers potential override comparison + $loweredDefaults = array_change_key_case($this->defaultHeaders, CASE_LOWER); + foreach($headers as $k => $v) { + if (array_key_exists(strtolower($k), $loweredDefaults)) { + unset($headers[$k]); + } + } + return array_merge( + $headers, + $this->defaultHeaders ); + } + + private function request() + { + $headers = $this->headers; if ($this->lastEventId !== '') { $headers['Last-Event-ID'] = $this->lastEventId; } diff --git a/tests/EventSourceTest.php b/tests/EventSourceTest.php index e698a61..ad7fc2c 100644 --- a/tests/EventSourceTest.php +++ b/tests/EventSourceTest.php @@ -51,6 +51,48 @@ public function testConstructorCanBeCalledWithoutBrowser() $this->assertInstanceOf('Clue\React\Buzz\Browser', $browser); } + + public function testConstructorCanBeCalledWithoutCustomHeaders() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $es = new EventSource('http://example.valid', $loop); + + $ref = new ReflectionProperty($es, 'headers'); + $ref->setAccessible(true); + $headers = $ref->getValue($es); + + $ref = new ReflectionProperty($es, 'defaultHeaders'); + $ref->setAccessible(true); + $defaultHeaders = $ref->getValue($es); + + $this->assertEquals($defaultHeaders, $headers); + } + + public function testConstructorCanBeCalledWithCustomHeaders() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $es = new EventSource('http://example.valid', $loop, null, array( + 'x-custom' => '1234', + 'Cache-Control' => 'only-if-cached', + 'ACCEPT' => 'no-store', + 'cache-control' => 'none' + )); + + $ref = new ReflectionProperty($es, 'headers'); + $ref->setAccessible(true); + $headers = $ref->getValue($es); + + // Could have used the defaultHeaders property on EventSource, + // but this ensures the defaults are not altered by hardcoding their values in this test + $this->assertEquals(array( + 'Accept' => 'text/event-stream', + 'Cache-Control' => 'no-cache', + 'x-custom' => '1234' + ), $headers); + } + public function testConstructorWillSendGetRequestThroughGivenBrowser() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();