Skip to content

Commit d1dcf66

Browse files
[HttpClient] add PluggableHttpClient to allow processing the response stream
1 parent 2ed6a0d commit d1dcf66

12 files changed

+639
-188
lines changed

src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,12 @@ public function getError(): ?string
110110
/**
111111
* @return bool Whether the wrapped error has been thrown or not
112112
*/
113-
public function didThrow(): bool
113+
public function didThrow(bool $didThrow = null): bool
114114
{
115+
if (null !== $didThrow && $this->didThrow !== $didThrow) {
116+
return !$this->didThrow = $didThrow;
117+
}
118+
115119
return $this->didThrow;
116120
}
117121

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Symfony\Component\HttpClient\Response\PluggableResponse;
15+
use Symfony\Component\HttpClient\Response\ResponseStream;
16+
use Symfony\Contracts\HttpClient\HttpClientInterface;
17+
use Symfony\Contracts\HttpClient\ResponseInterface;
18+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
19+
20+
/**
21+
* Allows processing responses while streaming them.
22+
*
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*/
25+
class PluggableHttpClient implements HttpClientInterface
26+
{
27+
private $client;
28+
private $pluggableResponseFactory;
29+
30+
public function __construct(HttpClientInterface $client, callable $pluggableResponseFactory)
31+
{
32+
$this->client = $client;
33+
$this->pluggableResponseFactory = $pluggableResponseFactory;
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function request(string $method, string $url, array $options = []): ResponseInterface
40+
{
41+
$response = ($this->pluggableResponseFactory)($this->client, $method, $url, $options);
42+
43+
if (!$response instanceof PluggableResponse) {
44+
throw new \TypeError(sprintf('The response factory passed to "%s" must return a "%s", "%s" found.', self::class, PluggableResponse::class, get_debug_type($response)));
45+
}
46+
47+
return $response;
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function stream($responses, float $timeout = null): ResponseStreamInterface
54+
{
55+
if ($responses instanceof PluggableResponse) {
56+
$responses = [$responses];
57+
} elseif (!is_iterable($responses)) {
58+
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of PluggableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
59+
}
60+
61+
return new ResponseStream(PluggableResponse::stream($responses, $timeout));
62+
}
63+
}

src/Symfony/Component/HttpClient/Response/AmpResponse.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
*/
3535
final class AmpResponse implements ResponseInterface
3636
{
37+
use CommonResponseTrait;
3738
use ResponseTrait;
3839

3940
private $multi;
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Response;
13+
14+
use Symfony\Component\HttpClient\Exception\ClientException;
15+
use Symfony\Component\HttpClient\Exception\JsonException;
16+
use Symfony\Component\HttpClient\Exception\RedirectionException;
17+
use Symfony\Component\HttpClient\Exception\ServerException;
18+
use Symfony\Component\HttpClient\Exception\TransportException;
19+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
20+
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
21+
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
22+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
23+
24+
/**
25+
* Implements common logic for response classes.
26+
*
27+
* @author Nicolas Grekas <p@tchwork.com>
28+
*
29+
* @internal
30+
*/
31+
trait CommonResponseTrait
32+
{
33+
/**
34+
* @var callable|null A callback that tells whether we're waiting for response headers
35+
*/
36+
private $initializer;
37+
private $shouldBuffer;
38+
private $content;
39+
private $offset = 0;
40+
private $jsonData;
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function getContent(bool $throw = true): string
46+
{
47+
if ($this->initializer) {
48+
self::initialize($this);
49+
}
50+
51+
if ($throw) {
52+
$this->checkStatusCode();
53+
}
54+
55+
if (null === $this->content) {
56+
$content = null;
57+
58+
foreach (self::stream([$this]) as $chunk) {
59+
if (!$chunk->isLast()) {
60+
$content .= $chunk->getContent();
61+
}
62+
}
63+
64+
if (null !== $content) {
65+
return $content;
66+
}
67+
68+
if ('HEAD' === $this->getInfo('http_method') || \in_array($this->getInfo('http_code'), [204, 304], true)) {
69+
return '';
70+
}
71+
72+
throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
73+
}
74+
75+
foreach (self::stream([$this]) as $chunk) {
76+
// Chunks are buffered in $this->content already
77+
}
78+
79+
rewind($this->content);
80+
81+
return stream_get_contents($this->content);
82+
}
83+
84+
/**
85+
* {@inheritdoc}
86+
*/
87+
public function toArray(bool $throw = true): array
88+
{
89+
if ('' === $content = $this->getContent($throw)) {
90+
throw new TransportException('Response body is empty.');
91+
}
92+
93+
if (null !== $this->jsonData) {
94+
return $this->jsonData;
95+
}
96+
97+
$contentType = $this->headers['content-type'][0] ?? 'application/json';
98+
99+
if (!preg_match('/\bjson\b/i', $contentType)) {
100+
throw new JsonException(sprintf('Response content-type is "%s" while a JSON-compatible one was expected for "%s".', $contentType, $this->getInfo('url')));
101+
}
102+
103+
try {
104+
$content = json_decode($content, true, 512, JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? JSON_THROW_ON_ERROR : 0));
105+
} catch (\JsonException $e) {
106+
throw new JsonException($e->getMessage().sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
107+
}
108+
109+
if (\PHP_VERSION_ID < 70300 && JSON_ERROR_NONE !== json_last_error()) {
110+
throw new JsonException(json_last_error_msg().sprintf(' for "%s".', $this->getInfo('url')), json_last_error());
111+
}
112+
113+
if (!\is_array($content)) {
114+
throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
115+
}
116+
117+
if (null !== $this->content) {
118+
// Option "buffer" is true
119+
return $this->jsonData = $content;
120+
}
121+
122+
return $content;
123+
}
124+
125+
/**
126+
* Casts the response to a PHP stream resource.
127+
*
128+
* @return resource
129+
*
130+
* @throws TransportExceptionInterface When a network error occurs
131+
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
132+
* @throws ClientExceptionInterface On a 4xx when $throw is true
133+
* @throws ServerExceptionInterface On a 5xx when $throw is true
134+
*/
135+
public function toStream(bool $throw = true)
136+
{
137+
if ($throw) {
138+
// Ensure headers arrived
139+
$this->getHeaders($throw);
140+
}
141+
142+
$stream = StreamWrapper::createResource($this);
143+
stream_get_meta_data($stream)['wrapper_data']
144+
->bindHandles($this->handle, $this->content);
145+
146+
return $stream;
147+
}
148+
149+
/**
150+
* {@inheritdoc}
151+
*/
152+
public function cancel(): void
153+
{
154+
$this->info['canceled'] = true;
155+
$this->info['error'] = 'Response has been canceled.';
156+
$this->close();
157+
}
158+
159+
/**
160+
* Closes the response and all its network handles.
161+
*/
162+
abstract protected function close(): void;
163+
164+
private static function initialize(self $response): void
165+
{
166+
if (null !== $response->getInfo('error')) {
167+
throw new TransportException($response->getInfo('error'));
168+
}
169+
170+
try {
171+
if (($response->initializer)($response)) {
172+
foreach (self::stream([$response]) as $chunk) {
173+
if ($chunk->isFirst()) {
174+
break;
175+
}
176+
}
177+
}
178+
} catch (\Throwable $e) {
179+
// Persist timeouts thrown during initialization
180+
$response->info['error'] = $e->getMessage();
181+
$response->close();
182+
throw $e;
183+
}
184+
185+
$response->initializer = null;
186+
}
187+
188+
private function checkStatusCode()
189+
{
190+
$code = $this->getInfo('http_code');
191+
192+
if (500 <= $code) {
193+
throw new ServerException($this);
194+
}
195+
196+
if (400 <= $code) {
197+
throw new ClientException($this);
198+
}
199+
200+
if (300 <= $code) {
201+
throw new RedirectionException($this);
202+
}
203+
}
204+
}

src/Symfony/Component/HttpClient/Response/CurlResponse.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@
2626
*/
2727
final class CurlResponse implements ResponseInterface
2828
{
29-
use ResponseTrait {
29+
use CommonResponseTrait {
3030
getContent as private doGetContent;
3131
}
32+
use ResponseTrait;
3233

3334
private static $performing = false;
3435
private $multi;

src/Symfony/Component/HttpClient/Response/MockResponse.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*/
2626
class MockResponse implements ResponseInterface
2727
{
28+
use CommonResponseTrait;
2829
use ResponseTrait {
2930
doDestruct as public __destruct;
3031
}

src/Symfony/Component/HttpClient/Response/NativeResponse.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*/
2626
final class NativeResponse implements ResponseInterface
2727
{
28+
use CommonResponseTrait;
2829
use ResponseTrait;
2930

3031
private $context;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy