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 96bdf08

Browse filesBrowse files
[HttpClient] fix support for 103 Early Hints and other informational status codes
1 parent f48ebfa commit 96bdf08
Copy full SHA for 96bdf08

File tree

11 files changed

+138
-20
lines changed
Filter options

11 files changed

+138
-20
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
+7-13Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\HttpClient\Chunk\FirstChunk;
1616
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
1717
use Symfony\Component\HttpClient\Exception\TransportException;
18+
use Symfony\Component\HttpClient\HttpClientTrait;
1819
use Symfony\Component\HttpClient\Internal\ClientState;
1920
use Symfony\Contracts\HttpClient\ResponseInterface;
2021

@@ -25,6 +26,7 @@
2526
*/
2627
class MockResponse implements ResponseInterface
2728
{
29+
use HttpClientTrait;
2830
use ResponseTrait {
2931
doDestruct as public __destruct;
3032
}
@@ -45,21 +47,13 @@ class MockResponse implements ResponseInterface
4547
public function __construct($body = '', array $info = [])
4648
{
4749
$this->body = is_iterable($body) ? $body : (string) $body;
48-
$this->info = $info + $this->info;
50+
$this->info = $info + ['http_code' => 200] + $this->info;
4951

50-
if (!isset($info['response_headers'])) {
51-
return;
52+
if ($responseHeaders = $info['response_headers'] ?? []) {
53+
$this->info['response_headers'] = [];
54+
$responseHeaders = array_merge(...array_values(self::normalizeHeaders($responseHeaders)));
55+
self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
5256
}
53-
54-
$responseHeaders = [];
55-
56-
foreach ($info['response_headers'] as $k => $v) {
57-
foreach ((array) $v as $v) {
58-
$responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
59-
}
60-
}
61-
62-
$this->info['response_headers'] = $responseHeaders;
6357
}
6458

6559
/**

‎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.