[HttpClient] fix support for 103 Early Hints and other informational status codes
This commit is contained in:
parent
f48ebfa402
commit
34275bba1c
@ -20,8 +20,8 @@ use Symfony\Contracts\HttpClient\ChunkInterface;
|
|||||||
*/
|
*/
|
||||||
class DataChunk implements ChunkInterface
|
class DataChunk implements ChunkInterface
|
||||||
{
|
{
|
||||||
private $offset;
|
private $offset = 0;
|
||||||
private $content;
|
private $content = '';
|
||||||
|
|
||||||
public function __construct(int $offset = 0, string $content = '')
|
public function __construct(int $offset = 0, string $content = '')
|
||||||
{
|
{
|
||||||
@ -53,6 +53,14 @@ class DataChunk implements ChunkInterface
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getInformationalStatus(): ?array
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -65,6 +65,15 @@ class ErrorChunk implements ChunkInterface
|
|||||||
throw new TransportException($this->errorMessage, 0, $this->error);
|
throw new TransportException($this->errorMessage, 0, $this->error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getInformationalStatus(): ?array
|
||||||
|
{
|
||||||
|
$this->didThrow = true;
|
||||||
|
throw new TransportException($this->errorMessage, 0, $this->error);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Chunk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class InformationalChunk extends DataChunk
|
||||||
|
{
|
||||||
|
private $status;
|
||||||
|
|
||||||
|
public function __construct(int $statusCode, array $headers)
|
||||||
|
{
|
||||||
|
$this->status = [$statusCode, $headers];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getInformationalStatus(): ?array
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ namespace Symfony\Component\HttpClient\Response;
|
|||||||
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||||
|
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
|
||||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
use Symfony\Component\HttpClient\Internal\CurlClientState;
|
use Symfony\Component\HttpClient\Internal\CurlClientState;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
@ -311,8 +312,11 @@ final class CurlResponse implements ResponseInterface
|
|||||||
return \strlen($data);
|
return \strlen($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// End of headers: handle redirects and add to the activity list
|
// End of headers: handle informational responses, redirects, etc.
|
||||||
|
|
||||||
if (200 > $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE)) {
|
if (200 > $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE)) {
|
||||||
|
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
|
||||||
|
|
||||||
return \strlen($data);
|
return \strlen($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,7 +343,7 @@ final class CurlResponse implements ResponseInterface
|
|||||||
|
|
||||||
if ($statusCode < 300 || 400 <= $statusCode || curl_getinfo($ch, CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
|
if ($statusCode < 300 || 400 <= $statusCode || curl_getinfo($ch, CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
|
||||||
// Headers and redirects completed, time to get the response's body
|
// Headers and redirects completed, time to get the response's body
|
||||||
$multi->handlesActivity[$id] = [new FirstChunk()];
|
$multi->handlesActivity[$id][] = new FirstChunk();
|
||||||
|
|
||||||
if ('destruct' === $waitFor) {
|
if ('destruct' === $waitFor) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -45,7 +45,7 @@ class MockResponse implements ResponseInterface
|
|||||||
public function __construct($body = '', array $info = [])
|
public function __construct($body = '', array $info = [])
|
||||||
{
|
{
|
||||||
$this->body = is_iterable($body) ? $body : (string) $body;
|
$this->body = is_iterable($body) ? $body : (string) $body;
|
||||||
$this->info = $info + $this->info;
|
$this->info = $info + ['http_code' => 200] + $this->info;
|
||||||
|
|
||||||
if (!isset($info['response_headers'])) {
|
if (!isset($info['response_headers'])) {
|
||||||
return;
|
return;
|
||||||
@ -59,7 +59,8 @@ class MockResponse implements ResponseInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info['response_headers'] = $responseHeaders;
|
$this->info['response_headers'] = [];
|
||||||
|
self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,8 +17,6 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Nicolas Grekas <p@tchwork.com>
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
final class ResponseStream implements ResponseStreamInterface
|
final class ResponseStream implements ResponseStreamInterface
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,8 @@ use Symfony\Component\HttpClient\Exception\TransportException;
|
|||||||
use Symfony\Component\HttpClient\MockHttpClient;
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
use Symfony\Component\HttpClient\NativeHttpClient;
|
use Symfony\Component\HttpClient\NativeHttpClient;
|
||||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||||
|
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
@ -122,6 +124,41 @@ class MockHttpClientTest extends HttpClientTestCase
|
|||||||
$body = ['<1>', '', '<2>'];
|
$body = ['<1>', '', '<2>'];
|
||||||
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
|
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'testInformationalResponseStream':
|
||||||
|
$client = $this->createMock(HttpClientInterface::class);
|
||||||
|
$response = new MockResponse('Here the body', ['response_headers' => [
|
||||||
|
'HTTP/1.1 103 ',
|
||||||
|
'Link: </style.css>; rel=preload; as=style',
|
||||||
|
'HTTP/1.1 200 ',
|
||||||
|
'Date: foo',
|
||||||
|
'Content-Length: 13',
|
||||||
|
]]);
|
||||||
|
$client->method('request')->willReturn($response);
|
||||||
|
$client->method('stream')->willReturn(new ResponseStream((function () use ($response) {
|
||||||
|
$chunk = $this->createMock(ChunkInterface::class);
|
||||||
|
$chunk->method('getInformationalStatus')
|
||||||
|
->willReturn([103, ['link' => ['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script']]]);
|
||||||
|
|
||||||
|
yield $response => $chunk;
|
||||||
|
|
||||||
|
$chunk = $this->createMock(ChunkInterface::class);
|
||||||
|
$chunk->method('isFirst')->willReturn(true);
|
||||||
|
|
||||||
|
yield $response => $chunk;
|
||||||
|
|
||||||
|
$chunk = $this->createMock(ChunkInterface::class);
|
||||||
|
$chunk->method('getContent')->willReturn('Here the body');
|
||||||
|
|
||||||
|
yield $response => $chunk;
|
||||||
|
|
||||||
|
$chunk = $this->createMock(ChunkInterface::class);
|
||||||
|
$chunk->method('isLast')->willReturn(true);
|
||||||
|
|
||||||
|
yield $response => $chunk;
|
||||||
|
})()));
|
||||||
|
|
||||||
|
return $client;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MockHttpClient($responses);
|
return new MockHttpClient($responses);
|
||||||
|
@ -20,4 +20,9 @@ class NativeHttpClientTest extends HttpClientTestCase
|
|||||||
{
|
{
|
||||||
return new NativeHttpClient();
|
return new NativeHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInformationalResponseStream()
|
||||||
|
{
|
||||||
|
$this->markTestSkipped('NativeHttpClient doesn\'t support informational status codes.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
"psr/log": "^1.0",
|
"psr/log": "^1.0",
|
||||||
"symfony/http-client-contracts": "^1.1.6",
|
"symfony/http-client-contracts": "^1.1.7",
|
||||||
"symfony/polyfill-php73": "^1.11"
|
"symfony/polyfill-php73": "^1.11"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
@ -47,6 +47,13 @@ interface ChunkInterface
|
|||||||
*/
|
*/
|
||||||
public function isLast(): bool;
|
public function isLast(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [status code, headers] tuple when a 1xx status code was just received.
|
||||||
|
*
|
||||||
|
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
|
||||||
|
*/
|
||||||
|
public function getInformationalStatus(): ?array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the content of the response chunk.
|
* Returns the content of the response chunk.
|
||||||
*
|
*
|
||||||
|
@ -754,6 +754,27 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$this->assertSame(200, $response->getStatusCode());
|
$this->assertSame(200, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInformationalResponseStream()
|
||||||
|
{
|
||||||
|
$client = $this->getHttpClient(__FUNCTION__);
|
||||||
|
$response = $client->request('GET', 'http://localhost:8057/103');
|
||||||
|
|
||||||
|
$chunks = [];
|
||||||
|
foreach ($client->stream($response) as $chunk) {
|
||||||
|
$chunks[] = $chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame(103, $chunks[0]->getInformationalStatus()[0]);
|
||||||
|
$this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $chunks[0]->getInformationalStatus()[1]['link']);
|
||||||
|
$this->assertTrue($chunks[1]->isFirst());
|
||||||
|
$this->assertSame('Here the body', $chunks[2]->getContent());
|
||||||
|
$this->assertTrue($chunks[3]->isLast());
|
||||||
|
$this->assertNull($chunks[3]->getInformationalStatus());
|
||||||
|
|
||||||
|
$this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
|
||||||
|
$this->assertContains('Link: </style.css>; rel=preload; as=style', $response->getInfo('response_headers'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @requires extension zlib
|
* @requires extension zlib
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user