Skip to content

Commit

Permalink
Trim whitespace in header values
Browse files Browse the repository at this point in the history
As per RFC 7230#3.2.4:

> The field value does not include any leading or trailing whitespace: OWS
> occurring before the first non-whitespace octet of the field value or after
> the last non-whitespace octet of the field value ought to be excluded by
> parsers when extracting the field value from a header field.

HTTP header values cannot begin or end with spaces or tabs, thus we should
remove them from the representation of the message.

Signed-off-by: Tim Düsterhus <[email protected]>
  • Loading branch information
TimWolla committed Apr 6, 2022
1 parent 736ba5c commit 74c04d4
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/AbstractSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/MessageTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
21 changes: 21 additions & 0 deletions test/MessageTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,27 @@ public function testWithAddedHeaderAllowsHeaderContinuations(): void
$this->assertSame("value,\r\n second value", $message->getHeaderLine('X-Foo-Bar'));
}

/** @return non-empty-array<non-empty-string, array{non-empty-string}> */
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<non-empty-string, array{int|float}> */
public function numericHeaderValuesProvider(): array
{
Expand Down
24 changes: 24 additions & 0 deletions test/Request/SerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,30 @@ public function testCanDeserializeResponseWithHeaderContinuations($text) : void
$this->assertSame('Baz; Bat', $request->getHeaderLine('X-Foo-Bar'));
}

/** @return non-empty-array<non-empty-string, array{non-empty-string}> */
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 [
Expand Down
24 changes: 24 additions & 0 deletions test/Response/SerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@ public function testCanDeserializeResponseWithHeaderContinuations($text)
$this->assertSame('Baz; Bat', $response->getHeaderLine('X-Foo-Bar'));
}

/** @return non-empty-array<non-empty-string, array{non-empty-string}> */
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";
Expand Down

0 comments on commit 74c04d4

Please sign in to comment.