Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 29355c0

Browse filesBrowse 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 ------- 34275bb [HttpClient] fix support for 103 Early Hints and other informational status codes
2 parents 200281d + 34275bb commit 29355c0
Copy full SHA for 29355c0

11 files changed

+134
-9
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Chunk/DataChunk.php
+10-2Lines 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
*/

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php
+9Lines 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
*/
+35Lines 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 <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;
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+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Response/CurlResponse.php
+6-2Lines 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;

‎src/Symfony/Component/HttpClient/Response/MockResponse.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Response/MockResponse.php
+3-2Lines 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
/**

‎src/Symfony/Component/HttpClient/Response/ResponseStream.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Response/ResponseStream.php
-2Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
/**
1919
* @author Nicolas Grekas <p@tchwork.com>
20-
*
21-
* @internal
2220
*/
2321
final class ResponseStream implements ResponseStreamInterface
2422
{

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
+37Lines 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);

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/composer.json
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Contracts/HttpClient/ChunkInterface.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ public function isFirst(): bool;
4747
*/
4848
public function isLast(): bool;
4949

50+
/**
51+
* Returns a [status code, headers] tuple when a 1xx status code was just received.
52+
*
53+
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
54+
*/
55+
public function getInformationalStatus(): ?array;
56+
5057
/**
5158
* Returns the content of the response chunk.
5259
*

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

Copy file name to clipboardExpand all lines: src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,27 @@ 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]->getInformationalStatus()[0]);
768+
$this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $chunks[0]->getInformationalStatus()[1]['link']);
769+
$this->assertTrue($chunks[1]->isFirst());
770+
$this->assertSame('Here the body', $chunks[2]->getContent());
771+
$this->assertTrue($chunks[3]->isLast());
772+
$this->assertNull($chunks[3]->getInformationalStatus());
773+
774+
$this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
775+
$this->assertContains('Link: </style.css>; rel=preload; as=style', $response->getInfo('response_headers'));
776+
}
777+
757778
/**
758779
* @requires extension zlib
759780
*/

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.