Skip to content

Commit b3a69ad

Browse files
bug #33391 [HttpClient] fix support for 103 Early Hints and other informational status codes (nicolas-grekas)
This PR was merged into the 4.3 branch. Discussion ---------- [HttpClient] fix support for 103 Early Hints and other informational status codes | Q | A | ------------- | --- | Branch? | 4.3 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - I learned quite recently how 1xx status codes work in HTTP 1.1 when I discovered the [103 Early Hint](https://evertpot.com/http/103-early-hints) status code from [RFC8297](https://tools.ietf.org/html/rfc8297) This PR fixes support for them by adding a new `getInformationalStatus()` method on `ChunkInterface`. This means that you can now know about 1xx status code by using the `$client->stream()` method: ```php $response = $client->request('GET', '...'); foreach ($client->stream($response) as $chunk) { [$code, $headers] = $chunk->getInformationalStatus(); if (103 === $code) { // $headers['link'] contains the early hints defined in RFC8297 } // ... } ``` Commits ------- 34275bba1c [HttpClient] fix support for 103 Early Hints and other informational status codes
2 parents e07a148 + 1e57343 commit b3a69ad

File tree

9 files changed

+106
-9
lines changed

9 files changed

+106
-9
lines changed

Chunk/DataChunk.php

Lines changed: 10 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,14 @@ public function isLast(): bool
5353
return false;
5454
}
5555

56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function getInformationalStatus(): ?array
60+
{
61+
return null;
62+
}
63+
5664
/**
5765
* {@inheritdoc}
5866
*/

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 getInformationalStatus(): ?array
72+
{
73+
$this->didThrow = true;
74+
throw new TransportException($this->errorMessage, 0, $this->error);
75+
}
76+
6877
/**
6978
* {@inheritdoc}
7079
*/

Chunk/InformationalChunk.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
16+
*
17+
* @internal
18+
*/
19+
class InformationalChunk extends DataChunk
20+
{
21+
private $status;
22+
23+
public function __construct(int $statusCode, array $headers)
24+
{
25+
$this->status = [$statusCode, $headers];
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function getInformationalStatus(): ?array
32+
{
33+
return $this->status;
34+
}
35+
}

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;
@@ -311,8 +312,11 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
311312
return \strlen($data);
312313
}
313314

314-
// End of headers: handle redirects and add to the activity list
315+
// End of headers: handle informational responses, redirects, etc.
316+
315317
if (200 > $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE)) {
318+
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
319+
316320
return \strlen($data);
317321
}
318322

@@ -339,7 +343,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
339343

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

344348
if ('destruct' === $waitFor) {
345349
return 0;

Response/MockResponse.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class MockResponse implements ResponseInterface
4545
public function __construct($body = '', array $info = [])
4646
{
4747
$this->body = is_iterable($body) ? $body : (string) $body;
48-
$this->info = $info + $this->info;
48+
$this->info = $info + ['http_code' => 200] + $this->info;
4949

5050
if (!isset($info['response_headers'])) {
5151
return;
@@ -59,7 +59,8 @@ public function __construct($body = '', array $info = [])
5959
}
6060
}
6161

62-
$this->info['response_headers'] = $responseHeaders;
62+
$this->info['response_headers'] = [];
63+
self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
6364
}
6465

6566
/**

Response/ResponseStream.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
/**
1919
* @author Nicolas Grekas <[email protected]>
20-
*
21-
* @internal
2220
*/
2321
final class ResponseStream implements ResponseStreamInterface
2422
{

Tests/MockHttpClientTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\Component\HttpClient\MockHttpClient;
1616
use Symfony\Component\HttpClient\NativeHttpClient;
1717
use Symfony\Component\HttpClient\Response\MockResponse;
18+
use Symfony\Component\HttpClient\Response\ResponseStream;
19+
use Symfony\Contracts\HttpClient\ChunkInterface;
1820
use Symfony\Contracts\HttpClient\HttpClientInterface;
1921
use Symfony\Contracts\HttpClient\ResponseInterface;
2022

@@ -122,6 +124,41 @@ protected function getHttpClient(string $testCase): HttpClientInterface
122124
$body = ['<1>', '', '<2>'];
123125
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
124126
break;
127+
128+
case 'testInformationalResponseStream':
129+
$client = $this->createMock(HttpClientInterface::class);
130+
$response = new MockResponse('Here the body', ['response_headers' => [
131+
'HTTP/1.1 103 ',
132+
'Link: </style.css>; rel=preload; as=style',
133+
'HTTP/1.1 200 ',
134+
'Date: foo',
135+
'Content-Length: 13',
136+
]]);
137+
$client->method('request')->willReturn($response);
138+
$client->method('stream')->willReturn(new ResponseStream((function () use ($response) {
139+
$chunk = $this->createMock(ChunkInterface::class);
140+
$chunk->method('getInformationalStatus')
141+
->willReturn([103, ['link' => ['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script']]]);
142+
143+
yield $response => $chunk;
144+
145+
$chunk = $this->createMock(ChunkInterface::class);
146+
$chunk->method('isFirst')->willReturn(true);
147+
148+
yield $response => $chunk;
149+
150+
$chunk = $this->createMock(ChunkInterface::class);
151+
$chunk->method('getContent')->willReturn('Here the body');
152+
153+
yield $response => $chunk;
154+
155+
$chunk = $this->createMock(ChunkInterface::class);
156+
$chunk->method('isLast')->willReturn(true);
157+
158+
yield $response => $chunk;
159+
})()));
160+
161+
return $client;
125162
}
126163

127164
return new MockHttpClient($responses);

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
}

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": {

0 commit comments

Comments
 (0)