Skip to content

Commit 65d19bd

Browse files
committed
bug #45998 [HttpClient] Fix sending content-length when streaming the body (nicolas-grekas)
This PR was merged into the 4.4 branch. Discussion ---------- [HttpClient] Fix sending content-length when streaming the body | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #45965 | License | MIT | Doc PR | - Follows #45906 and previous PRs on the topic. This PR partly reverts previous changes but keeps tests added to cover how we manage content-related headers when redirecting. Relying on curl doesn't work in all cases, so we need to manage them on our own. Commits ------- ee57696 [HttpClient] Fix sending content-length when streaming the body
2 parents 2607b66 + ee57696 commit 65d19bd

File tree

5 files changed

+30
-19
lines changed

5 files changed

+30
-19
lines changed

src/Symfony/Component/HttpClient/CurlHttpClient.php

+14-11
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,7 @@ public function request(string $method, string $url, array $options = []): Respo
202202
$options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
203203
}
204204

205-
$hasContentLength = isset($options['normalized_headers']['content-length'][0]);
206-
207-
foreach ($options['headers'] as $i => $header) {
208-
if ($hasContentLength && 0 === stripos($header, 'Content-Length:')) {
209-
// Let curl handle Content-Length headers
210-
unset($options['headers'][$i]);
211-
continue;
212-
}
205+
foreach ($options['headers'] as $header) {
213206
if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
214207
// curl requires a special syntax to send empty headers
215208
$curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
@@ -236,7 +229,7 @@ public function request(string $method, string $url, array $options = []): Respo
236229
};
237230
}
238231

239-
if ($hasContentLength) {
232+
if (isset($options['normalized_headers']['content-length'][0])) {
240233
$curlopts[\CURLOPT_INFILESIZE] = substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
241234
} elseif (!isset($options['normalized_headers']['transfer-encoding'])) {
242235
$curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked'; // Enable chunked request bodies
@@ -249,7 +242,7 @@ public function request(string $method, string $url, array $options = []): Respo
249242
$curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
250243
}
251244
}
252-
} elseif ('' !== $body || 'POST' === $method || $hasContentLength) {
245+
} elseif ('' !== $body || 'POST' === $method) {
253246
$curlopts[\CURLOPT_POSTFIELDS] = $body;
254247
}
255248

@@ -406,16 +399,26 @@ private static function createRedirectResolver(array $options, string $host): \C
406399
}
407400
}
408401

409-
return static function ($ch, string $location) use ($redirectHeaders) {
402+
return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders) {
410403
try {
411404
$location = self::parseUrl($location);
412405
} catch (InvalidArgumentException $e) {
413406
return null;
414407
}
415408

409+
if ($noContent && $redirectHeaders) {
410+
$filterContentHeaders = static function ($h) {
411+
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
412+
};
413+
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
414+
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
415+
}
416+
416417
if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
417418
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
418419
curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
420+
} elseif ($noContent && $redirectHeaders) {
421+
curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);
419422
}
420423

421424
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));

src/Symfony/Component/HttpClient/HttpClientTrait.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
9292
&& (string) \strlen($options['body']) !== substr($h = $options['normalized_headers']['content-length'][0] ?? '', 16)
9393
&& ('' !== $h || '' !== $options['body'])
9494
) {
95-
if (isset($options['normalized_headers']['transfer-encoding'])) {
95+
if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
9696
unset($options['normalized_headers']['transfer-encoding']);
9797
$options['body'] = self::dechunk($options['body']);
9898
}

src/Symfony/Component/HttpClient/NativeHttpClient.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function request(string $method, string $url, array $options = []): Respo
8585

8686
$options['body'] = self::getBodyAsString($options['body']);
8787

88-
if (isset($options['normalized_headers']['transfer-encoding'])) {
88+
if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
8989
unset($options['normalized_headers']['transfer-encoding']);
9090
$options['headers'] = array_merge(...array_values($options['normalized_headers']));
9191
$options['body'] = self::dechunk($options['body']);
@@ -397,7 +397,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar
397397
}
398398
}
399399

400-
return static function (NativeClientState $multi, ?string $location, $context) use ($redirectHeaders, $proxy, $noProxy, &$info, $maxRedirects, $onProgress): ?string {
400+
return static function (NativeClientState $multi, ?string $location, $context) use (&$redirectHeaders, $proxy, $noProxy, &$info, $maxRedirects, $onProgress): ?string {
401401
if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
402402
$info['redirect_url'] = null;
403403

@@ -431,7 +431,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar
431431
$info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
432432
$options['content'] = '';
433433
$filterContentHeaders = static function ($h) {
434-
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:');
434+
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
435435
};
436436
$options['header'] = array_filter($options['header'], $filterContentHeaders);
437437
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
361361
if (curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
362362
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
363363
} elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) {
364-
$info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
365364
curl_setopt($ch, \CURLOPT_POSTFIELDS, '');
366-
curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
367365
}
368366
}
369367

@@ -382,7 +380,12 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
382380
$info['redirect_url'] = null;
383381

384382
if (300 <= $statusCode && $statusCode < 400 && null !== $location) {
385-
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location)) {
383+
if ($noContent = 303 === $statusCode || ('POST' === $info['http_method'] && \in_array($statusCode, [301, 302], true))) {
384+
$info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
385+
curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
386+
}
387+
388+
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent)) {
386389
$options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
387390
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
388391
curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);

src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,16 @@ public function test304()
332332
$this->assertSame('', $response->getContent(false));
333333
}
334334

335-
public function testRedirects()
335+
/**
336+
* @testWith [[]]
337+
* [["Content-Length: 7"]]
338+
*/
339+
public function testRedirects(array $headers = [])
336340
{
337341
$client = $this->getHttpClient(__FUNCTION__);
338342
$response = $client->request('POST', 'http://localhost:8057/301', [
339343
'auth_basic' => 'foo:bar',
344+
'headers' => $headers,
340345
'body' => function () {
341346
yield 'foo=bar';
342347
},

0 commit comments

Comments
 (0)