Skip to content
Permalink
Browse files

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

…status codes
  • Loading branch information
nicolas-grekas committed Aug 30, 2019
1 parent 92ac848 commit 4ccf132621ea4374f06d703dbfa557c9047179cf
@@ -20,8 +20,8 @@
*/
class DataChunk implements ChunkInterface
{
private $offset;
private $content;
private $offset = 0;
private $content = '';

public function __construct(int $offset = 0, string $content = '')
{
@@ -53,6 +53,16 @@ public function isLast(): bool
return false;
}

/**
* {@inheritdoc}
*/
public function isInformational(?array &$headers = []): int
{
$headers = [];

return 0;
}

/**
* {@inheritdoc}
*/
@@ -65,6 +65,15 @@ public function isLast(): bool
throw new TransportException($this->errorMessage, 0, $this->error);
}

/**
* {@inheritdoc}
*/
public function isInformational(?array &$headers = []): int
{
$this->didThrow = true;
throw new TransportException($this->errorMessage, 0, $this->error);
}

/**
* {@inheritdoc}
*/
@@ -0,0 +1,39 @@
<?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 = 0;
private $headers = [];

public function __construct(int $status, array $headers)
{
$this->status = $status;
$this->headers = $headers;
}

/**
* {@inheritdoc}
*/
public function isInformational(?array &$headers = []): int
{
$headers = $this->headers;

return $this->status;
}
}
@@ -13,6 +13,7 @@

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;
@@ -314,8 +315,11 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
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);
}

@@ -342,7 +346,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &

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;
@@ -70,6 +70,10 @@ protected function getHttpClient(string $testCase): HttpClientInterface
$this->markTestSkipped("MockHttpClient doesn't timeout on destruct");
break;

case 'testInformationalResponseStream':
$this->markTestSkipped("MockHttpClient doesn't allow mocking informational chunks (yet)");
break;

case 'testGetRequest':
array_unshift($headers, 'HTTP/1.1 200 OK');
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
@@ -20,4 +20,9 @@ protected function getHttpClient(string $testCase): HttpClientInterface
{
return new NativeHttpClient();
}

public function testInformationalResponseStream()
{
$this->markTestSkipped('NativeHttpClient doesn\'t support informational status codes.');
}
}
@@ -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": {
@@ -47,6 +47,15 @@ public function isFirst(): bool;
*/
public function isLast(): bool;

/**
* Tells when an 1xx status code was just received.
*
* @param array|null &$headers Set by reference to the headers of the informational response
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function isInformational(?array &$headers = []): int;

/**
* Returns the content of the response chunk.
*
@@ -754,6 +754,26 @@ public function testInformationalResponse()
$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]->isInformational($headers));
$this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $headers['link']);
$this->assertTrue($chunks[1]->isFirst());
$this->assertSame('Here the body', $chunks[2]->getContent());
$this->assertTrue($chunks[3]->isLast());

$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
*/

0 comments on commit 4ccf132

Please sign in to comment.
You can’t perform that action at this time.