diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index 9fcfa7ee9a561..973bb6107b96c 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * added `$response->toStream()` to cast responses to regular PHP streams * made `Psr18Client` implement relevant PSR-17 factories and have streaming responses * added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler + * allow enabling buffering conditionally with a Closure 4.3.0 ----- diff --git a/src/Symfony/Component/HttpClient/CachingHttpClient.php b/src/Symfony/Component/HttpClient/CachingHttpClient.php index 65426ef647437..3d7a1232f6f04 100644 --- a/src/Symfony/Component/HttpClient/CachingHttpClient.php +++ b/src/Symfony/Component/HttpClient/CachingHttpClient.php @@ -68,9 +68,8 @@ public function request(string $method, string $url, array $options = []): Respo { [$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true); $url = implode('', $url); - $options['extra']['no_cache'] = $options['extra']['no_cache'] ?? !$options['buffer']; - if (!empty($options['body']) || $options['extra']['no_cache'] || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) { + if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) { return $this->client->request($method, $url, $options); } diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 9ecd62b086e3b..bdd1992cf3a30 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -37,7 +37,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface use HttpClientTrait; use LoggerAwareTrait; - private $defaultOptions = self::OPTIONS_DEFAULTS + [ + private $defaultOptions = [ + 'buffer' => null, // bool|\Closure - a boolean or a closure telling if the response should be buffered based on its headers + ] + self::OPTIONS_DEFAULTS + [ 'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the // password as the second one; or string like username:password - enabling NTLM auth ]; @@ -62,8 +64,10 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.'); } + $this->defaultOptions['buffer'] = \Closure::fromCallable([__CLASS__, 'shouldBuffer']); + if ($defaultOptions) { - [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, self::OPTIONS_DEFAULTS); + [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); } $this->multi = $multi = new CurlClientState(); diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index c5c1cdb25f051..b42d2369c33df 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -503,4 +503,15 @@ private static function mergeQueryString(?string $queryString, array $queryArray return implode('&', $replace ? array_replace($query, $queryArray) : ($query + $queryArray)); } + + private static function shouldBuffer(array $headers): bool + { + $contentType = $headers['content-type'][0] ?? null; + + if (false !== $i = strpos($contentType, ';')) { + $contentType = substr($contentType, 0, $i); + } + + return $contentType && preg_match('#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', $contentType); + } } diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index f39d72c4501aa..35a26d9e6ca61 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -35,7 +35,9 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac use HttpClientTrait; use LoggerAwareTrait; - private $defaultOptions = self::OPTIONS_DEFAULTS; + private $defaultOptions = [ + 'buffer' => null, // bool|\Closure - a boolean or a closure telling if the response should be buffered based on its headers + ] + self::OPTIONS_DEFAULTS; /** @var NativeClientState */ private $multi; @@ -48,8 +50,10 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac */ public function __construct(array $defaultOptions = [], int $maxHostConnections = 6) { + $this->defaultOptions['buffer'] = \Closure::fromCallable([__CLASS__, 'shouldBuffer']); + if ($defaultOptions) { - [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, self::OPTIONS_DEFAULTS); + [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); } $this->multi = new NativeClientState(); diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index a064361763d3f..35aec9b09eb77 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -63,18 +63,18 @@ public function __construct(CurlClientState $multi, $ch, array $options = null, } if (null === $content = &$this->content) { - $content = ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : null; + $content = true === $options['buffer'] ? fopen('php://temp', 'w+') : null; } else { // Move the pushed response to the activity list if (ftell($content)) { rewind($content); $multi->handlesActivity[$id][] = stream_get_contents($content); } - $content = ($options['buffer'] ?? true) ? $content : null; + $content = true === $options['buffer'] ? $content : null; } - curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int { - return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger, &$content): int { + return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger, $content); }); if (null === $options) { @@ -280,7 +280,7 @@ private static function select(CurlClientState $multi, float $timeout): int /** * Parses header lines as curl yields them to us. */ - private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int + 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 { if (!\in_array($waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE), ['headers', 'destruct'], true)) { return \strlen($data); // Ignore HTTP trailers @@ -348,6 +348,10 @@ private static function parseHeaderLine($ch, string $data, array &$info, array & return 0; } + if ($options['buffer'] instanceof \Closure && !$content && $options['buffer']($headers)) { + $content = fopen('php://temp', 'w+'); + } + curl_setopt($ch, CURLOPT_PRIVATE, 'content'); } elseif (null !== $info['redirect_url'] && $logger) { $logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url'])); diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index fe94bc3436740..d5599f7765b13 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -103,7 +103,12 @@ public static function fromRequest(string $method, string $url, array $options, $response = new self([]); $response->requestOptions = $options; $response->id = ++self::$idSequence; - $response->content = ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : null; + + if (($options['buffer'] ?? null) instanceof \Closure) { + $response->content = $options['buffer']($mock->getHeaders(false)) ? fopen('php://temp', 'w+') : null; + } else { + $response->content = true === ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : null; + } $response->initializer = static function (self $response) { if (null !== $response->info['error']) { throw new TransportException($response->info['error']); diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index 7aa2d8022dc96..e0fb09327bff5 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -35,6 +35,7 @@ final class NativeResponse implements ResponseInterface private $inflate; private $multi; private $debugBuffer; + private $shouldBuffer; /** * @internal @@ -50,7 +51,8 @@ public function __construct(NativeClientState $multi, $context, string $url, $op $this->info = &$info; $this->resolveRedirect = $resolveRedirect; $this->onProgress = $onProgress; - $this->content = $options['buffer'] ? fopen('php://temp', 'w+') : null; + $this->content = true === $options['buffer'] ? fopen('php://temp', 'w+') : null; + $this->shouldBuffer = $options['buffer'] instanceof \Closure ? $options['buffer'] : null; // Temporary resources to dechunk/inflate the response stream $this->buffer = fopen('php://temp', 'w+'); @@ -92,6 +94,8 @@ public function getInfo(string $type = null) public function __destruct() { + $this->shouldBuffer = null; + try { $this->doDestruct(); } finally { @@ -152,6 +156,10 @@ private function open(): void stream_set_blocking($h, false); $this->context = $this->resolveRedirect = null; + if (null !== $this->shouldBuffer && null === $this->content && ($this->shouldBuffer)($this->headers)) { + $this->content = fopen('php://temp', 'w+'); + } + if (isset($context['ssl']['peer_certificate_chain'])) { $this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain']; } diff --git a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php index 4be27060038b5..c51b3d3c0564f 100644 --- a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php +++ b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php @@ -117,7 +117,7 @@ public function getContent(bool $throw = true): string } if (null === $content) { - throw new TransportException('Cannot get the content of the response twice: the request was issued with option "buffer" set to false.'); + throw new TransportException('Cannot get the content of the response twice: buffering is disabled.'); } return $content; diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index 1e7fe180a3a4e..fbe68d8c809fd 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpClient\Tests; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Contracts\HttpClient\Test\HttpClientTestCase as BaseHttpClientTestCase; abstract class HttpClientTestCase extends BaseHttpClientTestCase @@ -37,4 +38,21 @@ public function testToStream() $this->assertSame('', fread($stream, 1)); $this->assertTrue(feof($stream)); } + + public function testConditionalBuffering() + { + $client = $this->getHttpClient(__FUNCTION__); + $response = $client->request('GET', 'http://localhost:8057'); + $firstContent = $response->getContent(); + $secondContent = $response->getContent(); + + $this->assertSame($firstContent, $secondContent); + + $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () { return false; }]); + $response->getContent(); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Cannot get the content of the response twice: buffering is disabled.'); + $response->getContent(); + } } 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