[HttpClient] fix support for 103 Early Hints and other informational status codes

This commit is contained in:
Nicolas Grekas 2019-08-30 12:36:56 +02:00
parent f48ebfa402
commit 34275bba1c
11 changed files with 134 additions and 9 deletions

View File

@ -20,8 +20,8 @@ use Symfony\Contracts\HttpClient\ChunkInterface;
*/
class DataChunk implements ChunkInterface
{
private $offset;
private $content;
private $offset = 0;
private $content = '';
public function __construct(int $offset = 0, string $content = '')
{
@ -53,6 +53,14 @@ class DataChunk implements ChunkInterface
return false;
}
/**
* {@inheritdoc}
*/
public function getInformationalStatus(): ?array
{
return null;
}
/**
* {@inheritdoc}
*/

View File

@ -65,6 +65,15 @@ class ErrorChunk implements ChunkInterface
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}
*/

View File

@ -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;
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\HttpClient\Response;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\CurlClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;
@ -311,8 +312,11 @@ final class CurlResponse implements ResponseInterface
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)) {
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
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']) {
// Headers and redirects completed, time to get the response's body
$multi->handlesActivity[$id] = [new FirstChunk()];
$multi->handlesActivity[$id][] = new FirstChunk();
if ('destruct' === $waitFor) {
return 0;

View File

@ -45,7 +45,7 @@ class MockResponse implements ResponseInterface
public function __construct($body = '', array $info = [])
{
$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'])) {
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);
}
/**

View File

@ -17,8 +17,6 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class ResponseStream implements ResponseStreamInterface
{

View File

@ -15,6 +15,8 @@ use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\NativeHttpClient;
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\ResponseInterface;
@ -122,6 +124,41 @@ class MockHttpClientTest extends HttpClientTestCase
$body = ['<1>', '', '<2>'];
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
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);

View File

@ -20,4 +20,9 @@ class NativeHttpClientTest extends HttpClientTestCase
{
return new NativeHttpClient();
}
public function testInformationalResponseStream()
{
$this->markTestSkipped('NativeHttpClient doesn\'t support informational status codes.');
}
}

View File

@ -21,7 +21,7 @@
"require": {
"php": "^7.1.3",
"psr/log": "^1.0",
"symfony/http-client-contracts": "^1.1.6",
"symfony/http-client-contracts": "^1.1.7",
"symfony/polyfill-php73": "^1.11"
},
"require-dev": {

View File

@ -47,6 +47,13 @@ interface ChunkInterface
*/
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.
*

View File

@ -754,6 +754,27 @@ abstract class HttpClientTestCase extends TestCase
$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
*/