Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit db9b15a

Browse files
rjwebdevnicolas-grekas
authored andcommitted
[HttpClient] Allow enabling buffering conditionally with a Closure
1 parent 9ea05b1 commit db9b15a

10 files changed

+69
-14
lines changed

src/Symfony/Component/HttpClient/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* added `$response->toStream()` to cast responses to regular PHP streams
1212
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
1313
* added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
14+
* allow enabling buffering conditionally with a Closure
1415

1516
4.3.0
1617
-----

src/Symfony/Component/HttpClient/CachingHttpClient.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,8 @@ public function request(string $method, string $url, array $options = []): Respo
6868
{
6969
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
7070
$url = implode('', $url);
71-
$options['extra']['no_cache'] = $options['extra']['no_cache'] ?? !$options['buffer'];
7271

73-
if (!empty($options['body']) || $options['extra']['no_cache'] || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
72+
if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
7473
return $this->client->request($method, $url, $options);
7574
}
7675

src/Symfony/Component/HttpClient/CurlHttpClient.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
3737
use HttpClientTrait;
3838
use LoggerAwareTrait;
3939

40-
private $defaultOptions = self::OPTIONS_DEFAULTS + [
40+
private $defaultOptions = [
41+
'buffer' => null, // bool|\Closure - a boolean or a closure telling if the response should be buffered based on its headers
42+
] + self::OPTIONS_DEFAULTS + [
4143
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
4244
// password as the second one; or string like username:password - enabling NTLM auth
4345
];
@@ -62,8 +64,10 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
6264
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
6365
}
6466

67+
$this->defaultOptions['buffer'] = \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
68+
6569
if ($defaultOptions) {
66-
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, self::OPTIONS_DEFAULTS);
70+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
6771
}
6872

6973
$this->multi = $multi = new CurlClientState();

src/Symfony/Component/HttpClient/HttpClientTrait.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,4 +503,15 @@ private static function mergeQueryString(?string $queryString, array $queryArray
503503

504504
return implode('&', $replace ? array_replace($query, $queryArray) : ($query + $queryArray));
505505
}
506+
507+
private static function shouldBuffer(array $headers): bool
508+
{
509+
$contentType = $headers['content-type'][0] ?? null;
510+
511+
if (false !== $i = strpos($contentType, ';')) {
512+
$contentType = substr($contentType, 0, $i);
513+
}
514+
515+
return $contentType && preg_match('#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', $contentType);
516+
}
506517
}

src/Symfony/Component/HttpClient/NativeHttpClient.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
3535
use HttpClientTrait;
3636
use LoggerAwareTrait;
3737

38-
private $defaultOptions = self::OPTIONS_DEFAULTS;
38+
private $defaultOptions = [
39+
'buffer' => null, // bool|\Closure - a boolean or a closure telling if the response should be buffered based on its headers
40+
] + self::OPTIONS_DEFAULTS;
3941

4042
/** @var NativeClientState */
4143
private $multi;
@@ -48,8 +50,10 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
4850
*/
4951
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
5052
{
53+
$this->defaultOptions['buffer'] = \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
54+
5155
if ($defaultOptions) {
52-
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, self::OPTIONS_DEFAULTS);
56+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
5357
}
5458

5559
$this->multi = new NativeClientState();

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,18 @@ public function __construct(CurlClientState $multi, $ch, array $options = null,
6363
}
6464

6565
if (null === $content = &$this->content) {
66-
$content = ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : null;
66+
$content = true === $options['buffer'] ? fopen('php://temp', 'w+') : null;
6767
} else {
6868
// Move the pushed response to the activity list
6969
if (ftell($content)) {
7070
rewind($content);
7171
$multi->handlesActivity[$id][] = stream_get_contents($content);
7272
}
73-
$content = ($options['buffer'] ?? true) ? $content : null;
73+
$content = true === $options['buffer'] ? $content : null;
7474
}
7575

76-
curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
77-
return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
76+
curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger, &$content): int {
77+
return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger, $content);
7878
});
7979

8080
if (null === $options) {
@@ -280,7 +280,7 @@ private static function select(CurlClientState $multi, float $timeout): int
280280
/**
281281
* Parses header lines as curl yields them to us.
282282
*/
283-
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
283+
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger, &$content = null): int
284284
{
285285
if (!\in_array($waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE), ['headers', 'destruct'], true)) {
286286
return \strlen($data); // Ignore HTTP trailers
@@ -348,6 +348,10 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
348348
return 0;
349349
}
350350

351+
if ($options['buffer'] instanceof \Closure && !$content && $options['buffer']($headers)) {
352+
$content = fopen('php://temp', 'w+');
353+
}
354+
351355
curl_setopt($ch, CURLOPT_PRIVATE, 'content');
352356
} elseif (null !== $info['redirect_url'] && $logger) {
353357
$logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url']));

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,12 @@ public static function fromRequest(string $method, string $url, array $options,
103103
$response = new self([]);
104104
$response->requestOptions = $options;
105105
$response->id = ++self::$idSequence;
106-
$response->content = ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : null;
106+
107+
if (($options['buffer'] ?? null) instanceof \Closure) {
108+
$response->content = $options['buffer']($mock->getHeaders(false)) ? fopen('php://temp', 'w+') : null;
109+
} else {
110+
$response->content = true === ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : null;
111+
}
107112
$response->initializer = static function (self $response) {
108113
if (null !== $response->info['error']) {
109114
throw new TransportException($response->info['error']);

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ final class NativeResponse implements ResponseInterface
3535
private $inflate;
3636
private $multi;
3737
private $debugBuffer;
38+
private $shouldBuffer;
3839

3940
/**
4041
* @internal
@@ -50,7 +51,8 @@ public function __construct(NativeClientState $multi, $context, string $url, $op
5051
$this->info = &$info;
5152
$this->resolveRedirect = $resolveRedirect;
5253
$this->onProgress = $onProgress;
53-
$this->content = $options['buffer'] ? fopen('php://temp', 'w+') : null;
54+
$this->content = true === $options['buffer'] ? fopen('php://temp', 'w+') : null;
55+
$this->shouldBuffer = $options['buffer'] instanceof \Closure ? $options['buffer'] : null;
5456

5557
// Temporary resources to dechunk/inflate the response stream
5658
$this->buffer = fopen('php://temp', 'w+');
@@ -92,6 +94,8 @@ public function getInfo(string $type = null)
9294

9395
public function __destruct()
9496
{
97+
$this->shouldBuffer = null;
98+
9599
try {
96100
$this->doDestruct();
97101
} finally {
@@ -152,6 +156,10 @@ private function open(): void
152156
stream_set_blocking($h, false);
153157
$this->context = $this->resolveRedirect = null;
154158

159+
if (null !== $this->shouldBuffer && null === $this->content && ($this->shouldBuffer)($this->headers)) {
160+
$this->content = fopen('php://temp', 'w+');
161+
}
162+
155163
if (isset($context['ssl']['peer_certificate_chain'])) {
156164
$this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain'];
157165
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public function getContent(bool $throw = true): string
117117
}
118118

119119
if (null === $content) {
120-
throw new TransportException('Cannot get the content of the response twice: the request was issued with option "buffer" set to false.');
120+
throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
121121
}
122122

123123
return $content;

src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\HttpClient\Tests;
1313

14+
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
15+
use Symfony\Component\HttpClient\Exception\TransportException;
1416
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase as BaseHttpClientTestCase;
1517

1618
abstract class HttpClientTestCase extends BaseHttpClientTestCase
@@ -37,4 +39,21 @@ public function testToStream()
3739
$this->assertSame('', fread($stream, 1));
3840
$this->assertTrue(feof($stream));
3941
}
42+
43+
public function testConditionalBuffering()
44+
{
45+
$client = $this->getHttpClient(__FUNCTION__);
46+
$response = $client->request('GET', 'http://localhost:8057');
47+
$firstContent = $response->getContent();
48+
$secondContent = $response->getContent();
49+
50+
$this->assertSame($firstContent, $secondContent);
51+
52+
$response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () { return false; }]);
53+
$response->getContent();
54+
55+
$this->expectException(TransportException::class);
56+
$this->expectExceptionMessage('Cannot get the content of the response twice: buffering is disabled.');
57+
$response->getContent();
58+
}
4059
}

0 commit comments

Comments
 (0)