diff --git a/src/AbstractSerializer.php b/src/AbstractSerializer.php index b18ee6b4..ee4b3f88 100644 --- a/src/AbstractSerializer.php +++ b/src/AbstractSerializer.php @@ -95,7 +95,7 @@ protected static function splitStream(StreamInterface $stream) : array if (! isset($headers[$currentHeader])) { $headers[$currentHeader] = []; } - $headers[$currentHeader][] = ltrim($matches['value']); + $headers[$currentHeader][] = trim($matches['value'], "\t "); continue; } @@ -109,7 +109,7 @@ protected static function splitStream(StreamInterface $stream) : array // Append continuation to last header value found $value = array_pop($headers[$currentHeader]); - $headers[$currentHeader][] = $value . ' ' . ltrim($line); + $headers[$currentHeader][] = $value . ' ' . trim($line, "\t "); } // use RelativeStream to avoid copying initial stream into memory diff --git a/src/MessageTrait.php b/src/MessageTrait.php index f4fa0ec5..d12e2088 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -398,7 +398,8 @@ private function filterHeaderValue($values) : array return array_map(function ($value) { HeaderSecurity::assertValid($value); - return (string) $value; + // Remove optional whitespace (OWS, RFC 7230#3.2.3) around the header value. + return trim((string) $value, "\t "); }, array_values($values)); } diff --git a/test/MessageTraitTest.php b/test/MessageTraitTest.php index bbb39cef..e493f65d 100644 --- a/test/MessageTraitTest.php +++ b/test/MessageTraitTest.php @@ -336,6 +336,27 @@ public function testWithAddedHeaderAllowsHeaderContinuations(): void $this->assertSame("value,\r\n second value", $message->getHeaderLine('X-Foo-Bar')); } + /** @return non-empty-array */ + public function headersWithWhitespace(): array + { + return [ + 'no' => ["Baz"], + 'leading' => [" Baz"], + 'trailing' => ["Baz "], + 'both' => [" Baz "], + 'mixed' => [" \t Baz\t \t"], + ]; + } + + /** + * @dataProvider headersWithWhitespace + */ + public function testWithHeaderTrimsWhitespace(string $value): void + { + $message = $this->message->withHeader('X-Foo-Bar', $value); + $this->assertSame(trim($value, "\t "), $message->getHeaderLine('X-Foo-Bar')); + } + /** @return non-empty-array */ public function numericHeaderValuesProvider(): array { diff --git a/test/Request/SerializerTest.php b/test/Request/SerializerTest.php index 92af8003..2f9d9c3a 100644 --- a/test/Request/SerializerTest.php +++ b/test/Request/SerializerTest.php @@ -247,6 +247,30 @@ public function testCanDeserializeResponseWithHeaderContinuations($text) : void $this->assertSame('Baz; Bat', $request->getHeaderLine('X-Foo-Bar')); } + /** @return non-empty-array */ + public function headersWithWhitespace(): array + { + return [ + 'no' => ["POST /foo HTTP/1.0\r\nContent-Type: text/plain\r\nX-Foo-Bar:Baz\r\n\r\nContent!"], + 'leading' => ["POST /foo HTTP/1.0\r\nContent-Type: text/plain\r\nX-Foo-Bar: Baz\r\n\r\nContent!"], + 'trailing' => ["POST /foo HTTP/1.0\r\nContent-Type: text/plain\r\nX-Foo-Bar:Baz \r\n\r\nContent!"], + 'both' => ["POST /foo HTTP/1.0\r\nContent-Type: text/plain\r\nX-Foo-Bar: Baz \r\n\r\nContent!"], + 'mixed' => ["POST /foo HTTP/1.0\r\nContent-Type: text/plain\r\nX-Foo-Bar: \t Baz\t \t\r\n\r\nContent!"], + ]; + } + + /** + * @dataProvider headersWithWhitespace + */ + public function testDeserializationRemovesWhitespaceAroundValues(string $text): void + { + $request = Serializer::fromString($text); + + $this->assertInstanceOf(Request::class, $request); + + $this->assertSame('Baz', $request->getHeaderLine('X-Foo-Bar')); + } + public function messagesWithInvalidHeaders() : array { return [ diff --git a/test/Response/SerializerTest.php b/test/Response/SerializerTest.php index 0f3d456f..fce27a49 100644 --- a/test/Response/SerializerTest.php +++ b/test/Response/SerializerTest.php @@ -121,6 +121,30 @@ public function testCanDeserializeResponseWithHeaderContinuations($text) $this->assertSame('Baz; Bat', $response->getHeaderLine('X-Foo-Bar')); } + /** @return non-empty-array */ + public function headersWithWhitespace(): array + { + return [ + 'no' => ["HTTP/1.0 200 A-OK\r\nContent-Type: text/plain\r\nX-Foo-Bar:Baz\r\n\r\nContent!"], + 'leading' => ["HTTP/1.0 200 A-OK\r\nContent-Type: text/plain\r\nX-Foo-Bar: Baz\r\n\r\nContent!"], + 'trailing' => ["HTTP/1.0 200 A-OK\r\nContent-Type: text/plain\r\nX-Foo-Bar:Baz \r\n\r\nContent!"], + 'both' => ["HTTP/1.0 200 A-OK\r\nContent-Type: text/plain\r\nX-Foo-Bar: Baz \r\n\r\nContent!"], + 'mixed' => ["HTTP/1.0 200 A-OK\r\nContent-Type: text/plain\r\nX-Foo-Bar: \t Baz\t \t\r\n\r\nContent!"], + ]; + } + + /** + * @dataProvider headersWithWhitespace + */ + public function testDeserializationRemovesWhitespaceAroundValues(string $text): void + { + $response = Serializer::fromString($text); + + $this->assertInstanceOf(Response::class, $response); + + $this->assertSame('Baz', $response->getHeaderLine('X-Foo-Bar')); + } + public function testCanDeserializeResponseWithoutBody() { $text = "HTTP/1.0 204\r\nX-Foo-Bar: Baz";