Skip to content

Commit 2c1cc5c

Browse files
rjwebdevnicolas-grekas
authored andcommitted
[HttpClient] Allow enabling buffering based on the content-type of the response
1 parent cf57007 commit 2c1cc5c

File tree

10 files changed

+80
-14
lines changed

10 files changed

+80
-14
lines changed

src/Symfony/Component/HttpClient/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* added `$response->toStream()` to cast responses to regular PHP streams
1111
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
1212
* added `max_duration` option
13+
* made buffering depend on the response content-type by default
1314

1415
4.3.0
1516
-----

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: 4 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' => '#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', // bool|string - a boolean or a regexp telling which content-types should be buffered
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
];
@@ -63,7 +65,7 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
6365
}
6466

6567
if ($defaultOptions) {
66-
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, self::OPTIONS_DEFAULTS);
68+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
6769
}
6870

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

src/Symfony/Component/HttpClient/HttpClientTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
4242

4343
$options = self::mergeDefaultOptions($options, $defaultOptions, $allowExtraOptions);
4444

45+
if (\is_string($options['buffer'] ?? null) && false === @preg_match($options['buffer'], '')) {
46+
throw new InvalidArgumentException('Option "buffer" should be a boolean or a valid regular expression.');
47+
}
48+
4549
if (isset($options['json'])) {
4650
if (isset($options['body']) && '' !== $options['body']) {
4751
throw new InvalidArgumentException('Define either the "json" or the "body" option, setting both is not supported.');

src/Symfony/Component/HttpClient/NativeHttpClient.php

Lines changed: 4 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' => '#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', // bool|string - a boolean or a regexp telling which content-types should be buffered
40+
] + self::OPTIONS_DEFAULTS;
3941

4042
/** @var NativeClientState */
4143
private $multi;
@@ -49,7 +51,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
4951
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
5052
{
5153
if ($defaultOptions) {
52-
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, self::OPTIONS_DEFAULTS);
54+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
5355
}
5456

5557
$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) {
@@ -278,7 +278,7 @@ private static function select(CurlClientState $multi, float $timeout): int
278278
/**
279279
* Parses header lines as curl yields them to us.
280280
*/
281-
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
281+
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
282282
{
283283
if (!\in_array($waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE), ['headers', 'destruct'], true)) {
284284
return \strlen($data); // Ignore HTTP trailers
@@ -346,6 +346,10 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
346346
return 0;
347347
}
348348

349+
if (isset($headers['content-type']) && \is_string($options['buffer']) && !$content) {
350+
$content = self::createBufferForContentType($headers['content-type'][0], $options['buffer']);
351+
}
352+
349353
curl_setopt($ch, CURLOPT_PRIVATE, 'content');
350354
} elseif (null !== $info['redirect_url'] && $logger) {
351355
$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 (\is_string($options['buffer'] ?? null) && $contentType = $mock->getHeaders(false)['content-type'][0] ?? '') {
108+
$response->content = preg_match($options['buffer'], strtolower($contentType)) ? 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: 7 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 $bufferRegex;
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->bufferRegex = \is_string($options['buffer']) ? $options['buffer'] : null;
5456

5557
// Temporary resources to dechunk/inflate the response stream
5658
$this->buffer = fopen('php://temp', 'w+');
@@ -152,6 +154,10 @@ private function open(): void
152154
stream_set_blocking($h, false);
153155
$this->context = $this->resolveRedirect = null;
154156

157+
if (null !== $this->bufferRegex && isset($this->headers['content-type']) && null === $this->content) {
158+
$this->content = self::createBufferForContentType($this->headers['content-type'][0], $this->bufferRegex);
159+
}
160+
155161
if (isset($context['ssl']['peer_certificate_chain'])) {
156162
$this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain'];
157163
}

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

Lines changed: 13 additions & 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;
@@ -244,6 +244,18 @@ private static function addResponseHeaders(array $responseHeaders, array &$info,
244244
}
245245
}
246246

247+
/**
248+
* @return resource|null
249+
*/
250+
private static function createBufferForContentType(string $contentType, string $bufferRegex)
251+
{
252+
if (false !== $i = strpos($contentType, ';')) {
253+
$contentType = substr($contentType, 0, $i);
254+
}
255+
256+
return preg_match($bufferRegex, strtolower($contentType)) ? fopen('php://temp', 'w+') : null;
257+
}
258+
247259
private function checkStatusCode()
248260
{
249261
if (500 <= $this->info['http_code']) {

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

Lines changed: 31 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,33 @@ public function testToStream()
3739
$this->assertSame('', fread($stream, 1));
3840
$this->assertTrue(feof($stream));
3941
}
42+
43+
public function testJsonRegexBuffer()
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' => '/xml/']);
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+
}
59+
60+
public function testInvalidRegexBuffer()
61+
{
62+
$client = $this->getHttpClient(__FUNCTION__);
63+
64+
$this->expectException(InvalidArgumentException::class);
65+
66+
$response = $client->request('GET', 'http://localhost:8057', ['buffer' => '/([a-z]+\/json))/']);
67+
68+
$firstPosts = $response->getContent();
69+
$secondPosts = $response->getContent();
70+
}
4071
}

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