8000 [HttpClient] throw exception when AsyncDecoratorTrait gets an already consumed response by nicolas-grekas · Pull Request #41656 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[HttpClient] throw exception when AsyncDecoratorTrait gets an already consumed response #41656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/Symfony/Component/HttpClient/Response/AsyncResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;

private const FIRST_CHUNK_YIELDED = 1;
private const LAST_CHUNK_YIELDED = 2;

private $client;
private $response;
private $info = ['canceled' => false];
private $passthru;
private $stream;
private $lastYielded = false;
private $yieldedState;

/**
* @param ?callable(ChunkInterface, AsyncContext): ?\Iterator $passthru
Expand Down Expand Up @@ -272,6 +275,14 @@ public static function stream(iterable $responses, float $timeout = null, string
continue;
}

if (null !== $chunk->getError()) {
// no-op
} elseif ($chunk->isFirst()) {
$r->yieldedState = self::FIRST_CHUNK_YIELDED;
} elseif (self::FIRST_CHUNK_YIELDED !== $r->yieldedState && null === $chunk->getInformationalStatus()) {
throw new \LogicException(sprintf('Instance of "%s" is already consumed and cannot be managed by "%s". A decorated client should not call any of the response\'s methods in its "request()" method.', get_debug_type($response), $class ?? static::class));
}

foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
yield $r => $chunk;
}
Expand All @@ -282,9 +293,9 @@ public static function stream(iterable $responses, float $timeout = null, string
}

if (null === $chunk->getError() && $chunk->isLast()) {
$r->lastYielded = true;
$r->yieldedState = self::LAST_CHUNK_YIELDED;
}
if (null === $chunk->getError() && !$r->lastYielded && $r->response === $response && null !== $r->client) {
if (null === $chunk->getError() && self::LAST_CHUNK_YIELDED !== $r->yieldedState && $r->response === $response && null !== $r->client) {
throw new \LogicException('A chunk passthru must yield an "isLast()" chunk before ending a stream.');
}

Expand Down
31 changes: 29 additions & 2 deletions src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;

class AsyncDecoratorTraitTest extends NativeHttpClientTest
{
protected function getHttpClient(string $testCase, \Closure $chunkFilter = null): HttpClientInterface
protected function getHttpClient(string $testCase, \Closure $chunkFilter = null, HttpClientInterface $decoratedClient = null): HttpClientInterface
{
if ('testHandleIsRemovedOnException' === $testCase) {
$this->markTestSkipped("AsyncDecoratorTrait doesn't cache handles");
}

$chunkFilter = $chunkFilter ?? static function (ChunkInterface $chunk, AsyncContext $context) { yield $chunk; };

return new class(parent::getHttpClient($testCase), $chunkFilter) implements HttpClientInterface {
return new class($decoratedClient ?? parent::getHttpClient($testCase), $chunkFilter) implements HttpClientInterface {
use AsyncDecoratorTrait;

private $chunkFilter;
Expand Down Expand Up @@ -303,4 +304,30 @@ public function testMultipleYieldInInitializer()
$this->assertSame(404, $response->getStatusCode());
$this->assertStringContainsString('injectedFoo', $response->getContent(false));
}

public function testConsumingDecoratedClient()
{
$client = $this->getHttpClient(__FUNCTION__, null, new class(parent::getHttpClient(__FUNCTION__)) implements HttpClientInterface {
use AsyncDecoratorTrait;

public function request(string $method, string $url, array $options = []): ResponseInterface
{
$response = $this->client->request($method, $url, $options);
$response->getStatusCode(); // should be avoided and breaks compatibility with AsyncDecoratorTrait

return $response;
}

public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
});

$response = $client->request('GET', 'http://localhost:8057/');

$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Instance of "Symfony\Component\HttpClient\Response\NativeResponse" is already consumed and cannot be managed by "Symfony\Component\HttpClient\Response\AsyncResponse". A decorated client should not call any of the response\'s methods in its "request()" method.');
$response->getStatusCode();
}
}
0