Skip to content

Commit 2f505db

Browse files
committed
Extract headers parsing into separate class.
1 parent 76a5de9 commit 2f505db

File tree

6 files changed

+170
-44
lines changed

6 files changed

+170
-44
lines changed

src/ResponseParser.php

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace Http\Curl;
33

4+
use Http\Curl\Tools\HeadersParser;
45
use Http\Message\MessageFactory;
56
use Http\Message\StreamFactory;
67
use Psr\Http\Message\ResponseInterface;
@@ -49,40 +50,19 @@ public function __construct(MessageFactory $messageFactory, StreamFactory $strea
4950
* @param array $info cURL response info
5051
*
5152
* @return ResponseInterface
53+
*
54+
* @throws \InvalidArgumentException
55+
* @throws \RuntimeException
5256
*/
5357
public function parse($raw, array $info)
5458
{
5559
$response = $this->messageFactory->createResponse();
5660

5761
$headerSize = $info['header_size'];
5862
$rawHeaders = substr($raw, 0, $headerSize);
59-
$headers = $this->parseRawHeaders($rawHeaders);
60-
61-
foreach ($headers as $header) {
62-
$header = trim($header);
63-
if ('' === $header) {
64-
continue;
65-
}
66-
67-
// Status line
68-
if (substr(strtolower($header), 0, 5) === 'http/') {
69-
$parts = explode(' ', $header, 3);
70-
$response = $response
71-
->withStatus($parts[1])
72-
->withProtocolVersion(substr($parts[0], 5));
73-
continue;
74-
}
7563

76-
// Extract header
77-
$parts = explode(':', $header, 2);
78-
$headerName = trim(urldecode($parts[0]));
79-
$headerValue = trim(urldecode($parts[1]));
80-
if ($response->hasHeader($headerName)) {
81-
$response = $response->withAddedHeader($headerName, $headerValue);
82-
} else {
83-
$response = $response->withHeader($headerName, $headerValue);
84-
}
85-
}
64+
$parser = new HeadersParser();
65+
$response = $parser->parseString($rawHeaders, $response);
8666

8767
/*
8868
* substr can return boolean value for empty string. But createStream does not support
@@ -94,22 +74,4 @@ public function parse($raw, array $info)
9474

9575
return $response;
9676
}
97-
98-
/**
99-
* Parse raw headers from HTTP response
100-
*
101-
* @param string $rawHeaders
102-
*
103-
* @return string[]
104-
*/
105-
private function parseRawHeaders($rawHeaders)
106-
{
107-
$allHeaders = explode("\r\n\r\n", $rawHeaders);
108-
$lastHeaders = trim(array_pop($allHeaders));
109-
while (count($allHeaders) > 0 && '' === $lastHeaders) {
110-
$lastHeaders = trim(array_pop($allHeaders));
111-
}
112-
$headers = explode("\r\n", $lastHeaders);
113-
return $headers;
114-
}
11577
}

src/Tools/HeadersParser.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
namespace Http\Curl\Tools;
3+
4+
use Psr\Http\Message\ResponseInterface;
5+
6+
/**
7+
* HTTP response headers parser
8+
*/
9+
class HeadersParser
10+
{
11+
/**
12+
* Parse headers and write them to response object.
13+
*
14+
* @param string[] $headers Response headers as array of header lines.
15+
* @param ResponseInterface $response Response to write headers to.
16+
*
17+
* @return ResponseInterface
18+
*
19+
* @throws \InvalidArgumentException For invalid status code arguments.
20+
* @throws \RuntimeException
21+
*/
22+
public function parseArray(array $headers, ResponseInterface $response)
23+
{
24+
$statusLine = trim(array_shift($headers));
25+
$parts = explode(' ', $statusLine, 3);
26+
if (count($parts) < 2 || substr(strtolower($parts[0]), 0, 5) !== 'http/') {
27+
throw new \RuntimeException(
28+
sprintf('"%s" is not a valid HTTP status line', $statusLine)
29+
);
30+
}
31+
32+
$reasonPhrase = count($parts) > 2 ? $parts[2] : '';
33+
/** @var ResponseInterface $response */
34+
$response = $response
35+
->withStatus($parts[1], $reasonPhrase)
36+
->withProtocolVersion(substr($parts[0], 5));
37+
38+
foreach ($headers as $headerLine) {
39+
$headerLine = trim($headerLine);
40+
if ('' === $headerLine) {
41+
continue;
42+
}
43+
44+
$parts = explode(':', $headerLine, 2);
45+
if (count($parts) !== 2) {
46+
throw new \RuntimeException(
47+
sprintf('"%s" is not a valid HTTP header line', $headerLine)
48+
);
49+
}
50+
$name = trim(urldecode($parts[0]));
51+
$value = trim(urldecode($parts[1]));
52+
if ($response->hasHeader($name)) {
53+
$response = $response->withAddedHeader($name, $value);
54+
} else {
55+
$response = $response->withHeader($name, $value);
56+
}
57+
}
58+
59+
return $response;
60+
}
61+
62+
/**
63+
* Parse headers and write them to response object.
64+
*
65+
* @param string $headers Response headers as single string.
66+
* @param ResponseInterface $response Response to write headers to.
67+
*
68+
* @return ResponseInterface
69+
*
70+
* @throws \InvalidArgumentException if $headers is not a string on object with __toString()
71+
* @throws \RuntimeException
72+
*/
73+
public function parseString($headers, ResponseInterface $response)
74+
{
75+
if (!(is_string($headers)
76+
|| (is_object($headers) && method_exists($headers, '__toString')))
77+
) {
78+
throw new \InvalidArgumentException(
79+
sprintf(
80+
'%s expects parameter 1 to be a string, %s given',
81+
__METHOD__,
82+
is_object($headers) ? get_class($headers) : gettype($headers)
83+
)
84+
);
85+
}
86+
return $this->parseArray(explode("\r\n", $headers), $response);
87+
}
88+
}

tests/Tools/HeadersParserTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
namespace Http\Curl\Tests\Tools;
3+
4+
use Http\Curl\Tools\HeadersParser;
5+
use Http\Discovery\MessageFactoryDiscovery;
6+
7+
/**
8+
* @covers Http\Curl\Tools\HeadersParser
9+
*/
10+
class HeadersParserTest extends \PHPUnit_Framework_TestCase
11+
{
12+
/**
13+
* Test valid headers parsing
14+
*/
15+
public function testValidHeaders()
16+
{
17+
$headers = file_get_contents(__DIR__ . '/data/headers_valid.txt');
18+
$response = MessageFactoryDiscovery::find()->createResponse();
19+
$parser = new HeadersParser();
20+
$response = $parser->parseString($headers, $response);
21+
static::assertEquals(200, $response->getStatusCode());
22+
static::assertEquals('OK', $response->getReasonPhrase());
23+
static::assertEquals('text/html; charset=UTF-8', $response->getHeaderLine('Content-Type'));
24+
static::assertEquals(['foo=1234', 'bar=4321'], $response->getHeader('Set-Cookie'));
25+
}
26+
27+
/**
28+
* Test parsing headers with invalid status line
29+
*
30+
* @expectedException \RuntimeException
31+
* @expectedExceptionMessage "HTTP/1.1" is not a valid HTTP status line
32+
*/
33+
public function testInvalidStatusLine()
34+
{
35+
$headers = file_get_contents(__DIR__ . '/data/headers_invalid_status.txt');
36+
$response = MessageFactoryDiscovery::find()->createResponse();
37+
$parser = new HeadersParser();
38+
$parser->parseString($headers, $response);
39+
}
40+
41+
/**
42+
* Test parsing headers with invalid header line
43+
*
44+
* @expectedException \RuntimeException
45+
* @expectedExceptionMessage "Content-Type text/html" is not a valid HTTP header line
46+
*/
47+
public function testInvalidHeaderLine()
48+
{
49+
$headers = file_get_contents(__DIR__ . '/data/headers_invalid_header.txt');
50+
$response = MessageFactoryDiscovery::find()->createResponse();
51+
$parser = new HeadersParser();
52+
$parser->parseString($headers, $response);
53+
}
54+
55+
56+
/**
57+
* @expectedException \InvalidArgumentException
58+
* @expectedExceptionMessage HeadersParser::parseString expects parameter 1 to be a string, array given
59+
*/
60+
public function testInvalidArgument()
61+
{
62+
$response = MessageFactoryDiscovery::find()->createResponse();
63+
$parser = new HeadersParser();
64+
$parser->parseString([], $response);
65+
}
66+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
HTTP/1.1 200 OK
2+
Content-Type text/html
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
HTTP/1.1
2+
Content-Type: text/html; charset=UTF-8
3+
Set-Cookie: foo=1234
4+
Set-Cookie: bar=4321

tests/Tools/data/headers_valid.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
HTTP/1.1 200 OK
2+
Content-Type: text/html; charset=UTF-8
3+
Set-Cookie: foo=1234
4+
Set-Cookie: bar=4321

0 commit comments

Comments
 (0)