Skip to content

Commit b8aa780

Browse files
Merge branch '5.2' into 5.3
* 5.2: [HttpClient] throw exception when AsyncDecoratorTrait gets an already consumed response [Validator] Fix tests by making constraint options dumps order consistent
2 parents c7fd9db + f2ede9c commit b8aa780

File tree

2 files changed

+38
-5
lines changed

2 files changed

+38
-5
lines changed

Response/AsyncResponse.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface
3131
{
3232
use CommonResponseTrait;
3333

34+
private const FIRST_CHUNK_YIELDED = 1;
35+
private const LAST_CHUNK_YIELDED = 2;
36+
3437
private $client;
3538
private $response;
3639
private $info = ['canceled' => false];
3740
private $passthru;
3841
private $stream;
39-
private $lastYielded = false;
42+
private $yieldedState;
4043

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

278+
if (null !== $chunk->getError()) {
279+
// no-op
280+
} elseif ($chunk->isFirst()) {
281+
$r->yieldedState = self::FIRST_CHUNK_YIELDED;
282+
} elseif (self::FIRST_CHUNK_YIELDED !== $r->yieldedState && null === $chunk->getInformationalStatus()) {
283+
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));
284+
}
285+
275286
foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
276287
yield $r => $chunk;
277288
}
@@ -282,9 +293,9 @@ public static function stream(iterable $responses, float $timeout = null, string
282293
}
283294

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

Tests/AsyncDecoratorTraitTest.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HttpClient\Tests;
1313

1414
use Symfony\Component\HttpClient\AsyncDecoratorTrait;
15+
use Symfony\Component\HttpClient\DecoratorTrait;
1516
use Symfony\Component\HttpClient\Response\AsyncContext;
1617
use Symfony\Component\HttpClient\Response\AsyncResponse;
1718
use Symfony\Contracts\HttpClient\ChunkInterface;
@@ -22,15 +23,15 @@
2223

2324
class AsyncDecoratorTraitTest extends NativeHttpClientTest
2425
{
25-
protected function getHttpClient(string $testCase, \Closure $chunkFilter = null): HttpClientInterface
26+
protected function getHttpClient(string $testCase, \Closure $chunkFilter = null, HttpClientInterface $decoratedClient = null): HttpClientInterface
2627
{
2728
if ('testHandleIsRemovedOnException' === $testCase) {
2829
$this->markTestSkipped("AsyncDecoratorTrait doesn't cache handles");
2930
}
3031

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

33-
return new class(parent::getHttpClient($testCase), $chunkFilter) implements HttpClientInterface {
34+
return new class($decoratedClient ?? parent::getHttpClient($testCase), $chunkFilter) implements HttpClientInterface {
3435
use AsyncDecoratorTrait;
3536

3637
private $chunkFilter;
@@ -303,4 +304,25 @@ public function testMultipleYieldInInitializer()
303304
$this->assertSame(404, $response->getStatusCode());
304305
$this->assertStringContainsString('injectedFoo', $response->getContent(false));
305306
}
307+
308+
public function testConsumingDecoratedClient()
309+
{
310+
$client = $this->getHttpClient(__FUNCTION__, null, new class(parent::getHttpClient(__FUNCTION__)) implements HttpClientInterface {
311+
use DecoratorTrait;
312+
313+
public function request(string $method, string $url, array $options = []): ResponseInterface
314+
{
315+
$response = $this->client->request($method, $url, $options);
316+
$response->getStatusCode(); // should be avoided and breaks compatibility with AsyncDecoratorTrait
317+
318+
return $response;
319+
}
320+
});
321+
322+
$response = $client->request('GET', 'http://localhost:8057/');
323+
324+
$this->expectException(\LogicException::class);
325+
$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.');
326+
$response->getStatusCode();
327+
}
306328
}

0 commit comments

Comments
 (0)