Skip to content

Commit f3d614c

Browse files
Merge branch '7.2' into 7.3
* 7.2: [Process] Fix process status tracking Prevent empty request body stream in HttplugClient and Psr18Client [Lock] Fix Predis error handling [HttpClient] Fix buffering AsyncResponse with no passthru [TypeInfo] Fix promoted property phpdoc reading [HttpClient] Fix retrying requests with Psr18Client and NTLM connections [HttpClient] Fix uploading files > 2GB [Mime] use isRendered method to avoid rendering an email twice
2 parents 8a0446d + c633647 commit f3d614c

File tree

15 files changed

+180
-64
lines changed

15 files changed

+180
-64
lines changed

src/Symfony/Bridge/Twig/Mime/BodyRenderer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function render(Message $message): void
4343
return;
4444
}
4545

46-
if (null === $message->getTextTemplate() && null === $message->getHtmlTemplate()) {
46+
if ($message->isRendered()) {
4747
// email has already been rendered
4848
return;
4949
}

src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,14 @@ public function testRenderedOnce()
105105
;
106106
$email->textTemplate('text');
107107

108+
$this->assertFalse($email->isRendered());
108109
$renderer->render($email);
110+
$this->assertTrue($email->isRendered());
111+
109112
$this->assertEquals('Text', $email->getTextBody());
110113

111114
$email->text('reset');
115+
$this->assertTrue($email->isRendered());
112116

113117
$renderer->render($email);
114118
$this->assertEquals('reset', $email->getTextBody());

src/Symfony/Component/HttpClient/CurlHttpClient.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,12 @@ public function request(string $method, string $url, array $options = []): Respo
238238
}
239239

240240
if (!\is_string($body)) {
241+
if (isset($options['auth_ntlm'])) {
242+
$curlopts[\CURLOPT_FORBID_REUSE] = true; // Reusing NTLM connections requires seeking capability, which only string bodies support
243+
}
244+
241245
if (\is_resource($body)) {
242-
$curlopts[\CURLOPT_INFILE] = $body;
246+
$curlopts[\CURLOPT_READDATA] = $body;
243247
} else {
244248
$curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
245249
static $eof = false;
@@ -318,6 +322,9 @@ public function request(string $method, string $url, array $options = []): Respo
318322
}
319323

320324
foreach ($curlopts as $opt => $value) {
325+
if (\CURLOPT_INFILESIZE === $opt && $value >= 1 << 31) {
326+
$opt = 115; // 115 === CURLOPT_INFILESIZE_LARGE, but it's not defined in PHP
327+
}
321328
if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
322329
$constantName = $this->findConstantName($opt);
323330
throw new TransportException(\sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
@@ -474,7 +481,7 @@ private function validateExtraCurlOptions(array $options): void
474481
\CURLOPT_RESOLVE => 'resolve',
475482
\CURLOPT_NOSIGNAL => 'timeout',
476483
\CURLOPT_HTTPHEADER => 'headers',
477-
\CURLOPT_INFILE => 'body',
484+
\CURLOPT_READDATA => 'body',
478485
\CURLOPT_READFUNCTION => 'body',
479486
\CURLOPT_INFILESIZE => 'body',
480487
\CURLOPT_POSTFIELDS => 'body',

src/Symfony/Component/HttpClient/HttplugClient.php

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,23 +224,44 @@ private function sendPsr7Request(RequestInterface $request, ?bool $buffer = null
224224
{
225225
try {
226226
$body = $request->getBody();
227+
$headers = $request->getHeaders();
227228

228-
if ($body->isSeekable()) {
229-
try {
230-
$body->seek(0);
231-
} catch (\RuntimeException) {
232-
// ignore
233-
}
229+
$size = $request->getHeader('content-length')[0] ?? -1;
230+
if (0 > $size && 0 < $size = $body->getSize() ?? -1) {
231+
$headers['Content-Length'] = [$size];
234232
}
235233

236-
$headers = $request->getHeaders();
237-
if (!$request->hasHeader('content-length') && 0 <= $size = $body->getSize() ?? -1) {
238-
$headers['Content-Length'] = [$size];
234+
if (0 === $size) {
235+
$body = '';
236+
} elseif (0 < $size && $size < 1 << 21) {
237+
if ($body->isSeekable()) {
238+
try {
239+
$body->seek(0);
240+
} catch (\RuntimeException) {
241+
// ignore
242+
}
243+
}
244+
245+
$body = $body->getContents();
246+
} else {
247+
$body = static function (int $size) use ($body) {
248+
if ($body->isSeekable()) {
249+
try {
250+
$body->seek(0);
251+
} catch (\RuntimeException) {
252+
// ignore
253+
}
254+
}
255+
256+
while (!$body->eof()) {
257+
yield $body->read($size);
258+
}
259+
};
239260
}
240261

241262
$options = [
242263
'headers' => $headers,
243-
'body' => static fn (int $size) => $body->read($size),
264+
'body' => $body,
244265
'buffer' => $buffer,
245266
];
246267

src/Symfony/Component/HttpClient/Psr18Client.php

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,44 @@ public function sendRequest(RequestInterface $request): ResponseInterface
8888
{
8989
try {
9090
$body = $request->getBody();
91+
$headers = $request->getHeaders();
9192

92-
if ($body->isSeekable()) {
93-
try {
94-
$body->seek(0);
95-
} catch (\RuntimeException) {
96-
// ignore
97-
}
93+
$size = $request->getHeader('content-length')[0] ?? -1;
94+
if (0 > $size && 0 < $size = $body->getSize() ?? -1) {
95+
$headers['Content-Length'] = [$size];
9896
}
9997

100-
$headers = $request->getHeaders();
101-
if (!$request->hasHeader('content-length') && 0 <= $size = $body->getSize() ?? -1) {
102-
$headers['Content-Length'] = [$size];
98+
if (0 === $size) {
99+
$body = '';
100+
} elseif (0 < $size && $size < 1 << 21) {
101+
if ($body->isSeekable()) {
102+
try {
103+
$body->seek(0);
104+
} catch (\RuntimeException) {
105+
// ignore
106+
}
107+
}
108+
109+
$body = $body->getContents();
110+
} else {
111+
$body = static function (int $size) use ($body) {
112+
if ($body->isSeekable()) {
113+
try {
114+
$body->seek(0);
115+
} catch (\RuntimeException) {
116+
// ignore
117+
}
118+
}
119+
120+
while (!$body->eof()) {
121+
yield $body->read($size);
122+
}
123+
};
103124
}
104125

105126
$options = [
106127
'headers' => $headers,
107-
'body' => static fn (int $size) => $body->read($size),
128+
'body' => $body,
108129
];
109130

110131
if ('1.0' === $request->getProtocolVersion()) {

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\HttpClient\Response;
1313

1414
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
15-
use Symfony\Component\HttpClient\Chunk\FirstChunk;
1615
use Symfony\Component\HttpClient\Chunk\LastChunk;
1716
use Symfony\Component\HttpClient\Exception\TransportException;
1817
use Symfony\Contracts\HttpClient\ChunkInterface;
@@ -245,7 +244,7 @@ public static function stream(iterable $responses, ?float $timeout = null, ?stri
245244
$wrappedResponses[] = $r->response;
246245

247246
if ($r->stream) {
248-
yield from self::passthruStream($response = $r->response, $r, new FirstChunk(), $asyncMap);
247+
yield from self::passthruStream($response = $r->response, $r, $asyncMap, new LastChunk());
249248

250249
if (!isset($asyncMap[$response])) {
251250
array_pop($wrappedResponses);
@@ -276,15 +275,9 @@ public static function stream(iterable $responses, ?float $timeout = null, ?stri
276275
}
277276

278277
if (!$r->passthru) {
279-
if (null !== $chunk->getError() || $chunk->isLast()) {
280-
unset($asyncMap[$response]);
281-
} elseif (null !== $r->content && '' !== ($content = $chunk->getContent()) && \strlen($content) !== fwrite($r->content, $content)) {
282-
$chunk = new ErrorChunk($r->offset, new TransportException(\sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
283-
$r->info['error'] = $chunk->getError();
284-
$r->response->cancel();
285-
}
278+
$r->stream = (static fn () => yield $chunk)();
279+
yield from self::passthruStream($response, $r, $asyncMap);
286280

287-
yield $r => $chunk;
288281
continue;
289282
}
290283

@@ -347,13 +340,13 @@ private static function passthru(HttpClientInterface $client, self $r, ChunkInte
347340
}
348341
$r->stream = $stream;
349342

350-
yield from self::passthruStream($response, $r, null, $asyncMap);
343+
yield from self::passthruStream($response, $r, $asyncMap);
351344
}
352345

353346
/**
354347
* @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
355348
*/
356-
private static function passthruStream(ResponseInterface $response, self $r, ?ChunkInterface $chunk, ?\SplObjectStorage $asyncMap): \Generator
349+
private static function passthruStream(ResponseInterface $response, self $r, ?\SplObjectStorage $asyncMap, ?ChunkInterface $chunk = null): \Generator
357350
{
358351
while (true) {
359352
try {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,20 @@ public function testBufferPurePassthru()
231231

232232
$this->assertStringContainsString('SERVER_PROTOCOL', $response->getContent());
233233
$this->assertStringContainsString('HTTP_HOST', $response->getContent());
234+
235+
$client = new class(parent::getHttpClient(__FUNCTION__)) implements HttpClientInterface {
236+
use AsyncDecoratorTrait;
237+
238+
public function request(string $method, string $url, array $options = []): ResponseInterface
239+
{
240+
return new AsyncResponse($this->client, $method, $url, $options);
241+
}
242+
};
243+
244+
$response = $client->request('GET', 'http://localhost:8057/');
245+
246+
$this->assertStringContainsString('SERVER_PROTOCOL', $response->getContent());
247+
$this->assertStringContainsString('HTTP_HOST', $response->getContent());
234248
}
235249

236250
public function testRetryTimeout()

src/Symfony/Component/Lock/Store/RedisStore.php

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Lock\Store;
1313

1414
use Predis\Response\Error;
15+
use Predis\Response\ServerException;
1516
use Relay\Relay;
1617
use Symfony\Component\Lock\Exception\InvalidTtlException;
1718
use Symfony\Component\Lock\Exception\LockConflictedException;
@@ -284,21 +285,18 @@ private function evaluate(string $script, string $resource, array $args): mixed
284285

285286
\assert($this->redis instanceof \Predis\ClientInterface);
286287

287-
$result = $this->redis->evalSha($scriptSha, 1, $resource, ...$args);
288-
if ($result instanceof Error && str_starts_with($result->getMessage(), self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) {
289-
$result = $this->redis->script('LOAD', $script);
290-
if ($result instanceof Error) {
291-
throw new LockStorageException($result->getMessage());
288+
try {
289+
return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args));
290+
} catch (LockStorageException $e) {
291+
// Fallthrough only if we need to load the script
292+
if (!str_starts_with($e->getMessage(), self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) {
293+
throw $e;
292294
}
293-
294-
$result = $this->redis->evalSha($scriptSha, 1, $resource, ...$args);
295295
}
296296

297-
if ($result instanceof Error) {
298-
throw new LockStorageException($result->getMessage());
299-
}
297+
$this->handlePredisError(fn () => $this->redis->script('LOAD', $script));
300298

301-
return $result;
299+
return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args));
302300
}
303301

304302
private function getUniqueToken(Key $key): string
@@ -347,4 +345,26 @@ private function getNowCode(): string
347345
now = math.floor(now * 1000)
348346
';
349347
}
348+
349+
/**
350+
* @template T
351+
*
352+
* @param callable(): T $callback
353+
*
354+
* @return T
355+
*/
356+
private function handlePredisError(callable $callback): mixed
357+
{
358+
try {
359+
$result = $callback();
360+
} catch (ServerException $e) {
361+
throw new LockStorageException($e->getMessage(), $e->getCode(), $e);
362+
}
363+
364+
if ($result instanceof Error) {
365+
throw new LockStorageException($result->getMessage());
366+
}
367+
368+
return $result;
369+
}
350370
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Lock\Tests\Store;
13+
14+
/**
15+
* @group integration
16+
*/
17+
class PredisStoreWithExceptionsTest extends AbstractRedisStoreTestCase
18+
{
19+
public static function setUpBeforeClass(): void
20+
{
21+
$redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => null]));
22+
try {
23+
$redis->connect();
24+
} catch (\Exception $e) {
25+
self::markTestSkipped($e->getMessage());
26+
}
27+
}
28+
29+
protected function getRedisConnection(): \Predis\Client
30+
{
31+
$redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => null]));
32+
$redis->connect();
33+
34+
return $redis;
35+
}
36+
}

src/Symfony/Component/Lock/Tests/Store/PredisStoreTest.php renamed to src/Symfony/Component/Lock/Tests/Store/PredisStoreWithoutExceptionsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*
1717
* @group integration
1818
*/
19-
class PredisStoreTest extends AbstractRedisStoreTestCase
19+
class PredisStoreWithoutExceptionsTest extends AbstractRedisStoreTestCase
2020
{
2121
public static function setUpBeforeClass(): void
2222
{

0 commit comments

Comments
 (0)