Skip to content

Commit 60f771b

Browse files
[HttpClient] fix support for 103 Early Hints and other informational status codes
1 parent 92ac848 commit 60f771b

File tree

9 files changed

+111
-5
lines changed

9 files changed

+111
-5
lines changed

src/Symfony/Component/HttpClient/Chunk/DataChunk.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
*/
2121
class DataChunk implements ChunkInterface
2222
{
23-
private $offset;
24-
private $content;
23+
private $offset = 0;
24+
private $content = '';
2525

2626
public function __construct(int $offset = 0, string $content = '')
2727
{
@@ -53,6 +53,16 @@ public function isLast(): bool
5353
return false;
5454
}
5555

56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function isInformational(?array &$headers = []): int
60+
{
61+
$headers = [];
62+
63+
return 0;
64+
}
65+
5666
/**
5767
* {@inheritdoc}
5868
*/

src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ public function isLast(): bool
6565
throw new TransportException($this->errorMessage, 0, $this->error);
6666
}
6767

68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function isInformational(?array &$headers = []): int
72+
{
73+
$this->didThrow = true;
74+
throw new TransportException($this->errorMessage, 0, $this->error);
75+
}
76+
6877
/**
6978
* {@inheritdoc}
7079
*/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Chunk;
13+
14+
/**
15+
* @author Nicolas Grekas <p@tchwork.com>
16+
*
17+
* @internal
18+
*/
19+
class InformationalChunk extends DataChunk
20+
{
21+
private $status = 0;
22+
private $headers = [];
23+
24+
public function __construct(int $status, array $headers)
25+
{
26+
$this->status = $status;
27+
$this->headers = $headers;
28+
}
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function isInformational(?array &$headers = []): int
34+
{
35+
$headers = $this->headers;
36+
37+
return $this->status;
38+
}
39+
}

src/Symfony/Component/HttpClient/Response/CurlResponse.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\HttpClient\Chunk\FirstChunk;
16+
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
1617
use Symfony\Component\HttpClient\Exception\TransportException;
1718
use Symfony\Component\HttpClient\Internal\CurlClientState;
1819
use Symfony\Contracts\HttpClient\ResponseInterface;
@@ -314,8 +315,11 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
314315
return \strlen($data);
315316
}
316317

317-
// End of headers: handle redirects and add to the activity list
318+
// End of headers: handle informational responses, redirects, etc.
319+
318320
if (200 > $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE)) {
321+
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
322+
319323
return \strlen($data);
320324
}
321325

@@ -342,7 +346,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
342346

343347
if ($statusCode < 300 || 400 <= $statusCode || curl_getinfo($ch, CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
344348
// Headers and redirects completed, time to get the response's body
345-
$multi->handlesActivity[$id] = [new FirstChunk()];
349+
$multi->handlesActivity[$id][] = new FirstChunk();
346350

347351
if ('destruct' === $waitFor) {
348352
return 0;

src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ protected function getHttpClient(string $testCase): HttpClientInterface
7070
$this->markTestSkipped("MockHttpClient doesn't timeout on destruct");
7171
break;
7272

73+
case 'testInformationalResponseStream':
74+
$this->markTestSkipped("MockHttpClient doesn't allow mocking informational chunks (yet)");
75+
break;
76+
7377
case 'testGetRequest':
7478
array_unshift($headers, 'HTTP/1.1 200 OK');
7579
$responses[] = new MockResponse($body, ['response_headers' => $headers]);

src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ protected function getHttpClient(string $testCase): HttpClientInterface
2020
{
2121
return new NativeHttpClient();
2222
}
23+
24+
public function testInformationalResponseStream()
25+
{
26+
$this->markTestSkipped('NativeHttpClient doesn\'t support informational status codes.');
27+
}
2328
}

src/Symfony/Component/HttpClient/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"require": {
2222
"php": "^7.1.3",
2323
"psr/log": "^1.0",
24-
"symfony/http-client-contracts": "^1.1.6",
24+
"symfony/http-client-contracts": "^1.1.7",
2525
"symfony/polyfill-php73": "^1.11"
2626
},
2727
"require-dev": {

src/Symfony/Contracts/HttpClient/ChunkInterface.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ public function isFirst(): bool;
4747
*/
4848
public function isLast(): bool;
4949

50+
/**
51+
* Tells when an 1xx status code was just received.
52+
*
53+
* @param array|null &$headers Set by reference to the headers of the informational response
54+
*
55+
* @return int The status code of the informational response or 0 if the response is not informational
56+
*
57+
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
58+
*/
59+
public function isInformational(?array &$headers = []): int;
60+
5061
/**
5162
* Returns the content of the response chunk.
5263
*

src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,30 @@ public function testInformationalResponse()
754754
$this->assertSame(200, $response->getStatusCode());
755755
}
756756

757+
public function testInformationalResponseStream()
758+
{
759+
$client = $this->getHttpClient(__FUNCTION__);
760+
$response = $client->request('GET', 'http://localhost:8057/103');
761+
762+
$chunks = [];
763+
foreach ($client->stream($response) as $chunk) {
764+
$chunks[] = $chunk;
765+
}
766+
767+
$this->assertSame(103, $chunks[0]->isInformational($headers));
768+
$this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $headers['link']);
769+
$this->assertTrue($chunks[1]->isFirst());
770+
$this->assertSame('Here the body', $chunks[2]->getContent());
771+
$this->assertTrue($chunks[3]->isLast());
772+
773+
$this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
774+
$this->assertContains('Link: </style.css>; rel=preload; as=style', $response->getInfo('response_headers'));
775+
776+
$headers = null;
777+
$this->assertSame(0, $chunks[3]->isInformational($headers));
778+
$this->assertSame([], $headers);
779+
}
780+
757781
/**
758782
* @requires extension zlib
759783
*/

0 commit comments

Comments
 (0)