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

Skip to content

Commit 9ac85d5

Browse files
[HttpClient] Preserve the case of headers when sending them
1 parent 85827f3 commit 9ac85d5

File tree

7 files changed

+83
-80
lines changed

7 files changed

+83
-80
lines changed

src/Symfony/Component/HttpClient/CurlHttpClient.php

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,14 @@ public function request(string $method, string $url, array $options = []): Respo
108108
if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
109109
unset($this->multi->pushedResponses[$url]);
110110
// Accept pushed responses only if their headers related to authentication match the request
111-
$expectedHeaders = [
112-
$options['headers']['authorization'] ?? null,
113-
$options['headers']['cookie'] ?? null,
114-
$options['headers']['x-requested-with'] ?? null,
115-
$options['headers']['range'] ?? null,
116-
];
111+
$expectedHeaders = ['authorization', 'cookie', 'x-requested-with', 'range'];
112+
foreach ($expectedHeaders as $k => $v) {
113+
$expectedHeaders[$k] = null;
114+
115+
foreach ($options['normalized_headers'][$v] ?? [] as $h) {
116+
$expectedHeaders[$k][] = substr($h, 2 + \strlen($v));
117+
}
118+
}
117119

118120
if ('GET' === $method && $expectedHeaders === $pushedResponse->headers && !$options['body']) {
119121
$this->logger && $this->logger->debug(sprintf('Connecting request to pushed response: "%s %s"', $method, $url));
@@ -206,11 +208,11 @@ public function request(string $method, string $url, array $options = []): Respo
206208
$curlopts[CURLOPT_NOSIGNAL] = true;
207209
}
208210

209-
if (!isset($options['headers']['accept-encoding'])) {
211+
if (!isset($options['normalized_headers']['accept-encoding'])) {
210212
$curlopts[CURLOPT_ENCODING] = ''; // Enable HTTP compression
211213
}
212214

213-
foreach ($options['request_headers'] as $header) {
215+
foreach ($options['headers'] as $header) {
214216
if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
215217
// curl requires a special syntax to send empty headers
216218
$curlopts[CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
@@ -221,7 +223,7 @@ public function request(string $method, string $url, array $options = []): Respo
221223

222224
// Prevent curl from sending its default Accept and Expect headers
223225
foreach (['accept', 'expect'] as $header) {
224-
if (!isset($options['headers'][$header])) {
226+
if (!isset($options['normalized_headers'][$header])) {
225227
$curlopts[CURLOPT_HTTPHEADER][] = $header.':';
226228
}
227229
}
@@ -237,9 +239,9 @@ public function request(string $method, string $url, array $options = []): Respo
237239
};
238240
}
239241

240-
if (isset($options['headers']['content-length'][0])) {
241-
$curlopts[CURLOPT_INFILESIZE] = $options['headers']['content-length'][0];
242-
} elseif (!isset($options['headers']['transfer-encoding'])) {
242+
if (isset($options['normalized_headers']['content-length'][0])) {
243+
$curlopts[CURLOPT_INFILESIZE] = substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
244+
} elseif (!isset($options['normalized_headers']['transfer-encoding'])) {
243245
$curlopts[CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked'; // Enable chunked request bodies
244246
}
245247

@@ -387,12 +389,12 @@ private static function createRedirectResolver(array $options, string $host): \C
387389
$redirectHeaders = [];
388390
if (0 < $options['max_redirects']) {
389391
$redirectHeaders['host'] = $host;
390-
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) {
392+
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
391393
return 0 !== stripos($h, 'Host:');
392394
});
393395

394-
if (isset($options['headers']['authorization']) || isset($options['headers']['cookie'])) {
395-
$redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) {
396+
if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
397+
$redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
396398
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
397399
});
398400
}

src/Symfony/Component/HttpClient/HttpClientTrait.php

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
5050
}
5151
$options['body'] = self::jsonEncode($options['json']);
5252
unset($options['json']);
53-
$options['headers']['content-type'] = $options['headers']['content-type'] ?? ['application/json'];
53+
54+
if (!isset($options['normalized_headers']['content-type'])) {
55+
$options['normalized_headers']['content-type'] = [$options['headers'][] = 'Content-Type: application/json'];
56+
}
5457
}
5558

5659
if (isset($options['body'])) {
@@ -61,19 +64,6 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
6164
$options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']);
6265
}
6366

64-
// Compute request headers
65-
$requestHeaders = $headers = [];
66-
67-
foreach ($options['headers'] as $name => $values) {
68-
foreach ($values as $value) {
69-
$requestHeaders[] = $name.': '.$headers[$name][] = $value = (string) $value;
70-
71-
if (\strlen($value) !== strcspn($value, "\r\n\0")) {
72-
throw new InvalidArgumentException(sprintf('Invalid header value: CR/LF/NUL found in "%s".', $value));
73-
}
74-
}
75-
}
76-
7767
// Validate on_progress
7868
if (!\is_callable($onProgress = $options['on_progress'] ?? 'var_dump')) {
7969
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, %s given.', \is_object($onProgress) ? \get_class($onProgress) : \gettype($onProgress)));
@@ -102,15 +92,14 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
10292

10393
if (null !== $url) {
10494
// Merge auth with headers
105-
if (($options['auth_basic'] ?? false) && !($headers['authorization'] ?? false)) {
106-
$requestHeaders[] = 'authorization: '.$headers['authorization'][] = 'Basic '.base64_encode($options['auth_basic']);
95+
if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
96+
$options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Basic '.base64_encode($options['auth_basic'])];
10797
}
10898
// Merge bearer with headers
109-
if (($options['auth_bearer'] ?? false) && !($headers['authorization'] ?? false)) {
110-
$requestHeaders[] = 'authorization: '.$headers['authorization'][] = 'Bearer '.$options['auth_bearer'];
99+
if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
100+
$options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Bearer '.$options['auth_bearer']];
111101
}
112102

113-
$options['request_headers'] = $requestHeaders;
114103
unset($options['auth_basic'], $options['auth_bearer']);
115104

116105
// Parse base URI
@@ -124,7 +113,6 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
124113
}
125114

126115
// Finalize normalization of options
127-
$options['headers'] = $headers;
128116
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
129117
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
130118

@@ -136,31 +124,38 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
136124
*/
137125
private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
138126
{
139-
unset($options['request_headers'], $defaultOptions['request_headers']);
140-
141-
$options['headers'] = self::normalizeHeaders($options['headers'] ?? []);
127+
$options['normalized_headers'] = self::normalizeHeaders($options['headers'] ?? []);
142128

143129
if ($defaultOptions['headers'] ?? false) {
144-
$options['headers'] += self::normalizeHeaders($defaultOptions['headers']);
130+
$options['normalized_headers'] += self::normalizeHeaders($defaultOptions['headers']);
145131
}
146132

147-
if ($options['resolve'] ?? false) {
148-
$options['resolve'] = array_change_key_case($options['resolve']);
133+
$options['headers'] = array_merge(...array_values($options['normalized_headers']) ?: [[]]);
134+
135+
if ($resolve = $options['resolve'] ?? false) {
136+
$options['resolve'] = [];
137+
foreach ($resolve as $k => $v) {
138+
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v;
139+
}
149140
}
150141

151142
// Option "query" is never inherited from defaults
152143
$options['query'] = $options['query'] ?? [];
153144

154145
foreach ($defaultOptions as $k => $v) {
155-
$options[$k] = $options[$k] ?? $v;
146+
if ('normalized_headers' !== $k && !isset($options[$k])) {
147+
$options[$k] = $v;
148+
}
156149
}
157150

158151
if (isset($defaultOptions['extra'])) {
159152
$options['extra'] += $defaultOptions['extra'];
160153
}
161154

162-
if ($defaultOptions['resolve'] ?? false) {
163-
$options['resolve'] += array_change_key_case($defaultOptions['resolve']);
155+
if ($resolve = $defaultOptions['resolve'] ?? false) {
156+
foreach ($resolve as $k => $v) {
157+
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v];
158+
}
164159
}
165160

166161
if ($allowExtraOptions || !$defaultOptions) {
@@ -169,7 +164,7 @@ private static function mergeDefaultOptions(array $options, array $defaultOption
169164

170165
// Look for unsupported options
171166
foreach ($options as $name => $v) {
172-
if (\array_key_exists($name, $defaultOptions)) {
167+
if (\array_key_exists($name, $defaultOptions) || 'normalized_headers' === $name) {
173168
continue;
174169
}
175170

@@ -188,9 +183,9 @@ private static function mergeDefaultOptions(array $options, array $defaultOption
188183
}
189184

190185
/**
191-
* Normalizes headers by putting their names as lowercased keys.
192-
*
193186
* @return string[][]
187+
*
188+
* @throws InvalidArgumentException When an invalid header is found
194189
*/
195190
private static function normalizeHeaders(array $headers): array
196191
{
@@ -204,10 +199,15 @@ private static function normalizeHeaders(array $headers): array
204199
$values = (array) $values;
205200
}
206201

207-
$normalizedHeaders[$name = strtolower($name)] = [];
202+
$lcName = strtolower($name);
203+
$normalizedHeaders[$lcName] = [];
208204

209205
foreach ($values as $value) {
210-
$normalizedHeaders[$name][] = $value;
206+
$normalizedHeaders[$lcName][] = $value = $name.': '.$value;
207+
208+
if (\strlen($value) !== strcspn($value, "\r\n\0")) {
209+
throw new InvalidArgumentException(sprintf('Invalid header: CR/LF/NUL found in "%s".', $value));
210+
}
211211
}
212212
}
213213

src/Symfony/Component/HttpClient/NativeHttpClient.php

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ public function request(string $method, string $url, array $options = []): Respo
7373

7474
$options['body'] = self::getBodyAsString($options['body']);
7575

76-
if ('' !== $options['body'] && 'POST' === $method && !isset($options['headers']['content-type'])) {
77-
$options['request_headers'][] = 'content-type: application/x-www-form-urlencoded';
76+
if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) {
77+
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
7878
}
7979

80-
if ($gzipEnabled = \extension_loaded('zlib') && !isset($options['headers']['accept-encoding'])) {
80+
if ($gzipEnabled = \extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
8181
// gzip is the most widely available algo, no need to deal with deflate
82-
$options['request_headers'][] = 'accept-encoding: gzip';
82+
$options['headers'][] = 'Accept-Encoding: gzip';
8383
}
8484

8585
if ($options['peer_fingerprint']) {
@@ -160,12 +160,12 @@ public function request(string $method, string $url, array $options = []): Respo
160160

161161
[$host, $port, $url['authority']] = self::dnsResolve($url, $this->multi, $info, $onProgress);
162162

163-
if (!isset($options['headers']['host'])) {
164-
$options['request_headers'][] = 'host: '.$host.$port;
163+
if (!isset($options['normalized_headers']['host'])) {
164+
$options['headers'][] = 'Host: '.$host.$port;
165165
}
166166

167-
if (!isset($options['headers']['user-agent'])) {
168-
$options['request_headers'][] = 'user-agent: Symfony HttpClient/Native';
167+
if (!isset($options['normalized_headers']['user-agent'])) {
168+
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
169169
}
170170

171171
$context = [
@@ -208,7 +208,7 @@ public function request(string $method, string $url, array $options = []): Respo
208208

209209
$resolveRedirect = self::createRedirectResolver($options, $host, $proxy, $noProxy, $info, $onProgress);
210210
$context = stream_context_create($context, ['notification' => $notification]);
211-
self::configureHeadersAndProxy($context, $host, $options['request_headers'], $proxy, $noProxy);
211+
self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, $noProxy);
212212

213213
return new NativeResponse($this->multi, $context, implode('', $url), $options, $gzipEnabled, $info, $resolveRedirect, $onProgress, $this->logger);
214214
}
@@ -335,12 +335,12 @@ private static function createRedirectResolver(array $options, string $host, ?ar
335335
$redirectHeaders = [];
336336
if (0 < $maxRedirects = $options['max_redirects']) {
337337
$redirectHeaders = ['host' => $host];
338-
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) {
338+
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
339339
return 0 !== stripos($h, 'Host:');
340340
});
341341

342-
if (isset($options['headers']['authorization']) || isset($options['headers']['cookie'])) {
343-
$redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) {
342+
if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
343+
$redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
344344
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
345345
});
346346
}
@@ -393,7 +393,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar
393393
if (false !== (parse_url($location, PHP_URL_HOST) ?? false)) {
394394
// Authorization and Cookie headers MUST NOT follow except for the initial host name
395395
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
396-
$requestHeaders[] = 'host: '.$host.$port;
396+
$requestHeaders[] = 'Host: '.$host.$port;
397397
self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, $noProxy);
398398
}
399399

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ private static function perform(NativeClientState $multi, array &$responses = nu
241241
try {
242242
// Notify the progress callback so that it can e.g. cancel
243243
// the request if the stream is inactive for too long
244+
$info['total_time'] = microtime(true) - $info['start_time'];
244245
$onProgress();
245246
} catch (\Throwable $e) {
246247
// no-op

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ public function provideRemoveDotSegments()
172172
public function testAuthBearerOption()
173173
{
174174
[, $options] = self::prepareRequest('POST', 'http://example.com', ['auth_bearer' => 'foobar'], HttpClientInterface::OPTIONS_DEFAULTS);
175-
$this->assertSame('Bearer foobar', $options['headers']['authorization'][0]);
176-
$this->assertSame('authorization: Bearer foobar', $options['request_headers'][0]);
175+
$this->assertSame(['Authorization: Bearer foobar'], $options['headers']);
176+
$this->assertSame(['Authorization: Bearer foobar'], $options['normalized_headers']['authorization']);
177177
}
178178

179179
/**
@@ -226,7 +226,7 @@ public function providePrepareAuthBasic()
226226
public function testPrepareAuthBasic($arg, $result)
227227
{
228228
[, $options] = $this->prepareRequest('POST', 'http://example.com', ['auth_basic' => $arg], HttpClientInterface::OPTIONS_DEFAULTS);
229-
$this->assertSame('Basic '.$result, $options['headers']['authorization'][0]);
229+
$this->assertSame('Authorization: Basic '.$result, $options['normalized_headers']['authorization'][0]);
230230
}
231231

232232
public function provideFingerprints()

0 commit comments

Comments
 (0)