From c72c125cd90918379ae515d8cede214b3379a146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 19 Feb 2017 13:49:39 +0100 Subject: [PATCH] Explicitly send Connection: close header for HTTP/1.1 messages Persistent connections (`Connection: keep-alive`) are currently not supported, so make sure we let the client know. --- README.md | 6 ++++++ src/Response.php | 18 ++++++++++++++++++ tests/ResponseTest.php | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/README.md b/README.md index 26e37536..5aec2a4c 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,12 @@ $response->writeHead(200, array( )); ``` +Note that persistent connections (`Connection: keep-alive`) are currently +not supported. +As such, HTTP/1.1 response messages will automatically include a +`Connection: close` header, irrespective of what header values are +passed explicitly. + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). diff --git a/src/Response.php b/src/Response.php index 952ae0d6..9483523d 100644 --- a/src/Response.php +++ b/src/Response.php @@ -172,6 +172,12 @@ public function writeContinue() * )); * ``` * + * Note that persistent connections (`Connection: keep-alive`) are currently + * not supported. + * As such, HTTP/1.1 response messages will automatically include a + * `Connection: close` header, irrespective of what header values are + * passed explicitly. + * * @param int $status * @param array $headers * @throws \Exception @@ -204,6 +210,18 @@ public function writeHead($status = 200, array $headers = array()) $this->chunkedEncoding = true; } + // HTTP/1.1 assumes persistent connection support by default + // we do not support persistent connections, so let the client know + if ($this->protocolVersion === '1.1') { + foreach($headers as $name => $value) { + if (strtolower($name) === 'connection') { + unset($headers[$name]); + } + } + + $headers['Connection'] = 'close'; + } + $data = $this->formatHead($status, $headers); $this->conn->write($data); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 6200934b..8c3e9077 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -12,6 +12,7 @@ public function testResponseShouldBeChunkedByDefault() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -51,6 +52,7 @@ public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -71,6 +73,7 @@ public function testResponseShouldNotBeChunkedWithContentLength() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Content-Length: 22\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -91,6 +94,7 @@ public function testResponseShouldNotBeChunkedWithContentLengthCaseInsensitive() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "CONTENT-LENGTH: 0\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -111,6 +115,7 @@ public function testResponseShouldIncludeCustomByPoweredAsFirstHeaderIfGivenExpl $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "Content-Length: 0\r\n"; $expected .= "X-POWERED-BY: demo\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -130,6 +135,7 @@ public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() $expected = ''; $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "Content-Length: 0\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -144,6 +150,27 @@ public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() $response->writeHead(200, array('Content-Length' => 0, 'X-Powered-By' => array())); } + public function testResponseShouldAlwaysIncludeConnectionCloseIrrespectiveOfExplicitValue() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "X-Powered-By: React/alpha\r\n"; + $expected .= "Content-Length: 0\r\n"; + $expected .= "Connection: close\r\n"; + $expected .= "\r\n"; + + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array('Content-Length' => 0, 'connection' => 'ignored')); + } + public function testResponseBodyShouldBeChunkedCorrectly() { $conn = $this @@ -283,6 +310,7 @@ public function shouldRemoveNewlinesFromHeaders() $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "FooBar: BazQux\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -304,6 +332,7 @@ public function missingStatusCodeTextShouldResultInNumberOnlyStatus() $expected .= "HTTP/1.1 700 \r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -327,6 +356,7 @@ public function shouldAllowArrayHeaderValues() $expected .= "Set-Cookie: foo=bar\r\n"; $expected .= "Set-Cookie: bar=baz\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -348,6 +378,7 @@ public function shouldIgnoreHeadersWithNullValues() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this