Skip to content

Commit 2f3cd38

Browse files
committed
:octocat: fix/improve HeaderUtil::normalize()
1 parent c4ae671 commit 2f3cd38

File tree

2 files changed

+87
-41
lines changed

2 files changed

+87
-41
lines changed

src/HeaderUtil.php

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,58 +30,49 @@ public static function normalize(iterable $headers):array{
3030

3131
foreach($headers as $key => $val){
3232

33-
// the key is numeric, so $val is either a string or an array
33+
// the key is numeric, so $val is either a string or an array that contains both
3434
if(is_numeric($key)){
35-
// "key: val"
36-
if(is_string($val)){
37-
$header = explode(':', $val, 2);
35+
[$key, $val] = self::normalizeKV($val);
3836

39-
if(count($header) !== 2){
40-
continue;
41-
}
42-
43-
$key = $header[0];
44-
$val = $header[1];
37+
if($key === null){
38+
continue;
4539
}
46-
// [$key, $val], ["key" => $key, "val" => $val]
47-
elseif(is_array($val) && !empty($val)){
48-
$key = array_keys($val)[0];
49-
$val = array_values($val)[0];
50-
51-
// skip if the key is numeric
52-
if(is_numeric($key)){
53-
continue;
40+
}
41+
42+
$key = self::normalizeHeaderName($key);
43+
44+
// cookie headers may appear multiple times - we'll just collect the last value here
45+
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2
46+
if($key === 'Set-Cookie'){
47+
$name = fn(string $v):string => trim(strtolower(explode('=', $v, 2)[0]));
48+
49+
// array received from Message::getHeaders()
50+
if(is_array($val)){
51+
foreach($val as $line){
52+
$normalized[$key][$name($line)] = trim($line);
5453
}
5554
}
5655
else{
57-
continue;
56+
$normalized[$key][$name($val)] = trim($val);
5857
}
5958
}
60-
// the key is named, so we assume $val holds the header values only, either as string or array
59+
// combine header fields with the same name
60+
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
6161
else{
62+
63+
// the key is named, so we assume $val holds the header values only, either as string or array
6264
if(is_array($val)){
6365
$val = implode(', ', array_values($val));
6466
}
65-
}
6667

67-
$key = self::normalizeHeaderName($key);
68-
$val = trim((string)($val ?? ''));
68+
$val = trim((string)($val ?? ''));
6969

70-
// skip if the header already exists but the current value is empty
71-
if(isset($normalized[$key]) && empty($val)){
72-
continue;
73-
}
70+
// skip if the header already exists but the current value is empty
71+
if(isset($normalized[$key]) && empty($val)){
72+
continue;
73+
}
7474

75-
// cookie headers may appear multiple times
76-
// https://tools.ietf.org/html/rfc6265#section-4.1.2
77-
if($key === 'Set-Cookie'){
78-
// we'll just collect the last value here and leave parsing up to you :P
79-
$normalized[$key][strtolower(explode('=', $val, 2)[0])] = $val;
80-
}
81-
// combine header fields with the same name
82-
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
83-
else{
84-
isset($normalized[$key]) && !empty($normalized[$key])
75+
!empty($normalized[$key])
8576
? $normalized[$key] .= ', '.$val
8677
: $normalized[$key] = $val;
8778
}
@@ -90,6 +81,33 @@ public static function normalize(iterable $headers):array{
9081
return $normalized;
9182
}
9283

84+
/**
85+
* Extracts a key:value pair from the given value and returns it as 2-element array.
86+
* If the key cannot be parsed, both array values will be `null`.
87+
*/
88+
protected static function normalizeKV(mixed $value):array{
89+
90+
// "key: val"
91+
if(is_string($value)){
92+
$kv = explode(':', $value, 2);
93+
94+
if(count($kv) === 2){
95+
return $kv;
96+
}
97+
}
98+
// [$key, $val], ["key" => $key, "val" => $val]
99+
elseif(is_array($value) && !empty($value)){
100+
$key = array_keys($value)[0];
101+
$val = array_values($value)[0];
102+
103+
if(is_string($key)){
104+
return [$key, $val];
105+
}
106+
}
107+
108+
return [null, null];
109+
}
110+
93111
/**
94112
* Trims whitespace from the header values.
95113
*

tests/HeaderUtilTest.php

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,30 @@ public function testNormalizeHeaders(array $headers, array $normalized):void{
4848
$this::assertSame($normalized, HeaderUtil::normalize($headers));
4949
}
5050

51+
public function testNormalizeHeadersFromMessageInterface():void{
52+
53+
$response = $this->responseFactory
54+
->createResponse()
55+
->withHeader('foo', 'bar')
56+
->withAddedHeader('what', 'nope')
57+
->withAddedHeader('what', 'why')
58+
->withHeader('set-cookie', 'foo=nope')
59+
->withAddedHeader('set-cookie', 'foo=bar')
60+
->withAddedHeader('set-cookie', 'what=why')
61+
;
62+
63+
$expected = [
64+
'Foo' => 'bar',
65+
'What' => 'nope, why',
66+
'Set-Cookie' => [
67+
'foo' => 'foo=bar',
68+
'what' => 'what=why',
69+
],
70+
];
71+
72+
$this::assertSame($expected, HeaderUtil::normalize($response->getHeaders()));
73+
}
74+
5175
public function testCombineHeaderFields():void{
5276

5377
$headers = [
@@ -61,23 +85,27 @@ public function testCombineHeaderFields():void{
6185
' x-foo ' => ['what', 'nope'],
6286
];
6387

64-
$this::assertSame([
88+
$expected = [
6589
'Accept' => 'foo, bar',
6690
'X-Whatever' => 'nope',
6791
'X-Foo' => 'bar, baz, what, nope',
68-
], HeaderUtil::normalize($headers));
92+
];
93+
94+
$this::assertSame($expected, HeaderUtil::normalize($headers));
6995

7096
$r = $this->responseFactory->createResponse();
7197

7298
foreach(HeaderUtil::normalize($headers) as $k => $v){
7399
$r = $r->withAddedHeader($k, $v);
74100
}
75101

76-
$this::assertSame([
102+
$expected = [
77103
'Accept' => ['foo, bar'],
78104
'X-Whatever' => ['nope'],
79105
'X-Foo' => ['bar, baz, what, nope'],
80-
], $r->getHeaders());
106+
];
107+
108+
$this::assertSame($expected, $r->getHeaders());
81109

82110
}
83111

0 commit comments

Comments
 (0)