Skip to content

Commit 75c69c1

Browse files
committed
feat: normalizes response headers
1 parent eb7d5e2 commit 75c69c1

File tree

2 files changed

+72
-21
lines changed

2 files changed

+72
-21
lines changed

src/Providers/Http/DTO/Response.php

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
* @phpstan-type ResponseArrayShape array{
1919
* statusCode: int,
20-
* headers: array<string, string|list<string>>,
20+
* headers: array<string, list<string>>,
2121
* body?: string|null,
2222
* reasonPhrase: string
2323
* }
@@ -37,10 +37,15 @@ class Response extends AbstractDataTransferObject
3737
protected int $statusCode;
3838

3939
/**
40-
* @var array<string, string|list<string>> The response headers.
40+
* @var array<string, list<string>> The response headers.
4141
*/
4242
protected array $headers;
4343

44+
/**
45+
* @var array<string, string> Map of lowercase header names to actual header names for fast lookup.
46+
*/
47+
protected array $headersMap;
48+
4449
/**
4550
* @var string|null The response body.
4651
*/
@@ -70,7 +75,8 @@ public function __construct(int $statusCode, array $headers, ?string $body = nul
7075
}
7176

7277
$this->statusCode = $statusCode;
73-
$this->headers = $headers;
78+
$this->headers = $this->normalizeHeaderValues($headers);
79+
$this->headersMap = $this->buildHeadersMap($this->headers);
7480
$this->body = $body;
7581
$this->reasonPhrase = $reasonPhrase;
7682
}
@@ -92,7 +98,7 @@ public function getStatusCode(): int
9298
*
9399
* @since n.e.x.t
94100
*
95-
* @return array<string, string|list<string>> The headers.
101+
* @return array<string, list<string>> The headers.
96102
*/
97103
public function getHeaders(): array
98104
{
@@ -105,17 +111,29 @@ public function getHeaders(): array
105111
* @since n.e.x.t
106112
*
107113
* @param string $name The header name (case-insensitive).
108-
* @return string|list<string>|null The header value(s) or null if not found.
114+
* @return list<string>|null The header value(s) or null if not found.
109115
*/
110-
public function getHeader(string $name)
116+
public function getHeader(string $name): ?array
111117
{
112-
// Case-insensitive header lookup
113-
foreach ($this->headers as $key => $value) {
114-
if (strcasecmp($key, $name) === 0) {
115-
return $value;
116-
}
118+
$lower = strtolower($name);
119+
if (!isset($this->headersMap[$lower])) {
120+
return null;
117121
}
118-
return null;
122+
return $this->headers[$this->headersMap[$lower]];
123+
}
124+
125+
/**
126+
* Gets the first value of a specific header.
127+
*
128+
* @since n.e.x.t
129+
*
130+
* @param string $name The header name (case-insensitive).
131+
* @return string|null The first header value or null if not found.
132+
*/
133+
public function getHeaderLine(string $name): ?string
134+
{
135+
$values = $this->getHeader($name);
136+
return $values !== null ? implode(', ', $values) : null;
119137
}
120138

121139
/**
@@ -154,6 +172,40 @@ public function isSuccessful(): bool
154172
return $this->statusCode >= 200 && $this->statusCode < 300;
155173
}
156174

175+
/**
176+
* Normalizes header values to ensure they are all arrays.
177+
*
178+
* @since n.e.x.t
179+
*
180+
* @param array<string, string|list<string>> $headers The headers to normalize.
181+
* @return array<string, list<string>> The normalized headers.
182+
*/
183+
private function normalizeHeaderValues(array $headers): array
184+
{
185+
$normalized = [];
186+
foreach ($headers as $name => $value) {
187+
$normalized[$name] = is_array($value) ? array_values($value) : [$value];
188+
}
189+
return $normalized;
190+
}
191+
192+
/**
193+
* Builds a map of lowercase header names to actual header names.
194+
*
195+
* @since n.e.x.t
196+
*
197+
* @param array<string, list<string>> $headers The headers.
198+
* @return array<string, string> The headers map.
199+
*/
200+
private function buildHeadersMap(array $headers): array
201+
{
202+
$map = [];
203+
foreach (array_keys($headers) as $name) {
204+
$map[strtolower($name)] = $name;
205+
}
206+
return $map;
207+
}
208+
157209
/**
158210
* Gets the response data as an array.
159211
*
@@ -199,13 +251,8 @@ public static function getJsonSchema(): array
199251
self::KEY_HEADERS => [
200252
'type' => 'object',
201253
'additionalProperties' => [
202-
'oneOf' => [
203-
['type' => 'string'],
204-
[
205-
'type' => 'array',
206-
'items' => ['type' => 'string'],
207-
],
208-
],
254+
'type' => 'array',
255+
'items' => ['type' => 'string'],
209256
],
210257
'description' => 'The response headers.',
211258
],

tests/unit/Providers/Http/HttpTransporterTest.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ public function testSendGetRequest(): void
8282
$this->assertInstanceOf(Response::class, $response);
8383
$this->assertEquals(200, $response->getStatusCode());
8484
$this->assertEquals('{"success":true}', $response->getBody());
85-
$this->assertEquals('application/json', $response->getHeader('Content-Type'));
85+
$this->assertEquals(['application/json'], $response->getHeader('Content-Type'));
86+
$this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));
87+
// Test case-insensitive header lookup
88+
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
89+
$this->assertEquals('application/json', $response->getHeaderLine('CONTENT-TYPE'));
8690
}
8791

8892
/**
@@ -112,7 +116,7 @@ public function testSendPostRequestWithBody(): void
112116
// Assert
113117
$this->assertEquals(201, $response->getStatusCode());
114118
$this->assertEquals('{"id":123}', $response->getBody());
115-
$this->assertEquals('/resource/123', $response->getHeader('Location'));
119+
$this->assertEquals(['/resource/123'], $response->getHeader('Location'));
116120

117121
// Verify the request was sent correctly
118122
$sentRequests = $this->mockClient->getRequests();

0 commit comments

Comments
 (0)