diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index c38a0a4f0cd6b..91b917f0458d4 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -38,7 +38,7 @@ class MockResponse implements ResponseInterface /** * @param string|string[]|iterable $body The response body as a string or an iterable of strings, * yielding an empty string simulates an idle timeout, - * exceptions are turned to TransportException + * throwing an exception yields an ErrorChunk * * @see ResponseInterface::getInfo() for possible info, e.g. "response_headers" */ @@ -183,6 +183,9 @@ protected static function perform(ClientState $multi, array &$responses): void $multi->handlesActivity[$id][] = null; $multi->handlesActivity[$id][] = $e; } + } elseif ($chunk instanceof \Throwable) { + $multi->handlesActivity[$id][] = null; + $multi->handlesActivity[$id][] = $chunk; } else { // Data or timeout chunk $multi->handlesActivity[$id][] = $chunk; @@ -275,16 +278,20 @@ private static function readResponse(self $response, array $options, ResponseInt $body = $mock instanceof self ? $mock->body : $mock->getContent(false); if (!\is_string($body)) { - foreach ($body as $chunk) { - if ('' === $chunk = (string) $chunk) { - // simulate an idle timeout - $response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url'])); - } else { - $response->body[] = $chunk; - $offset += \strlen($chunk); - // "notify" download progress - $onProgress($offset, $dlSize, $response->info); + try { + foreach ($body as $chunk) { + if ('' === $chunk = (string) $chunk) { + // simulate an idle timeout + $response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url'])); + } else { + $response->body[] = $chunk; + $offset += \strlen($chunk); + // "notify" download progress + $onProgress($offset, $dlSize, $response->info); + } } + } catch (\Throwable $e) { + $response->body[] = $e; } } elseif ('' !== $body) { $response->body[] = $body; diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php index f97b0cbb363ff..47234d71de0e9 100644 --- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php @@ -11,6 +11,9 @@ namespace Symfony\Component\HttpClient\Tests; +use Symfony\Component\HttpClient\Chunk\DataChunk; +use Symfony\Component\HttpClient\Chunk\ErrorChunk; +use Symfony\Component\HttpClient\Chunk\FirstChunk; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\NativeHttpClient; @@ -63,6 +66,46 @@ public function invalidResponseFactoryProvider() ]; } + public function testThrowExceptionInBodyGenerator() + { + $mockHttpClient = new MockHttpClient([ + new MockResponse((static function (): \Generator { + yield 'foo'; + throw new TransportException('foo ccc'); + })()), + new MockResponse((static function (): \Generator { + yield 'bar'; + throw new \RuntimeException('bar ccc'); + })()), + ]); + + try { + $mockHttpClient->request('GET', 'https://symfony.com', [])->getContent(); + $this->fail(); + } catch (TransportException $e) { + $this->assertEquals(new TransportException('foo ccc'), $e->getPrevious()); + $this->assertSame('foo ccc', $e->getMessage()); + } + + $chunks = []; + try { + foreach ($mockHttpClient->stream($mockHttpClient->request('GET', 'https://symfony.com', [])) as $chunk) { + $chunks[] = $chunk; + } + $this->fail(); + } catch (TransportException $e) { + $this->assertEquals(new \RuntimeException('bar ccc'), $e->getPrevious()); + $this->assertSame('bar ccc', $e->getMessage()); + } + + $this->assertCount(3, $chunks); + $this->assertEquals(new FirstChunk(0, ''), $chunks[0]); + $this->assertEquals(new DataChunk(0, 'bar'), $chunks[1]); + $this->assertInstanceOf(ErrorChunk::class, $chunks[2]); + $this->assertSame(3, $chunks[2]->getOffset()); + $this->assertSame('bar ccc', $chunks[2]->getError()); + } + protected function getHttpClient(string $testCase): HttpClientInterface { $responses = []; @@ -167,7 +210,7 @@ protected function getHttpClient(string $testCase): HttpClientInterface case 'testResolve': $responses[] = new MockResponse($body, ['response_headers' => $headers]); $responses[] = new MockResponse($body, ['response_headers' => $headers]); - $responses[] = new MockResponse((function () { throw new \Exception('Fake connection timeout'); yield ''; })(), ['response_headers' => $headers]); + $responses[] = new MockResponse((function () { yield ''; })(), ['response_headers' => $headers]); break; case 'testTimeoutOnStream':