Skip to content

Normalize array-valued Swoole request headers#235

Closed
ChiragAgg5k wants to merge 1 commit into0.34.xfrom
fix/swoole-multi-value-headers
Closed

Normalize array-valued Swoole request headers#235
ChiragAgg5k wants to merge 1 commit into0.34.xfrom
fix/swoole-multi-value-headers

Conversation

@ChiragAgg5k
Copy link
Copy Markdown
Member

Summary

  • normalize array-valued Swoole header entries before returning them from Request::getHeader()
  • keep getHeader() aligned with its declared string return type even when the underlying Swoole request stores repeated header values as arrays
  • add a regression test for scalar and multi-value Swoole headers

Problem

This was hit in production by Appwrite Edge while handling gateway-rewritten requests:

The failure mode is:

TypeError: Utopia\Http\Adapter\Swoole\Request::getHeader(): Return value must be of type string, array returned

The concrete trigger was Traefik's replacePath middleware. It stores the original path in X-Replaced-Path via Header.Add(...). If an inbound request already contains the same header, Swoole exposes the combined value as an array. getHeader() currently returns that raw array even though its contract is string.

Solution

  • lowercase the requested header name before lookup
  • normalize the resolved Swoole header value before returning it
  • when the header is multi-valued, return the last scalar/stringable entry

Returning the last value matches append-style middleware behavior, including the Traefik replacePath case where the middleware-added value is appended after any client-supplied value.

Why 0.34.x

This issue is currently affecting a consumer pinned to 0.34.*, so the fix is proposed against 0.34.x first to make it releasable for that dependency line.

Testing

  • vendor/bin/pint src/Http/Adapter/Swoole/Request.php tests/SwooleRequestTest.php
  • vendor/bin/phpunit tests/RequestTest.php tests/SwooleRequestTest.php

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 13, 2026

Greptile Summary

This PR fixes a production TypeError where Swoole exposes repeated request headers as arrays while getHeader() declares a string return type. The fix adds a normalizeHeaderValue() helper and lowercases the key in getHeader(). The normalization logic itself is correct, but the same key normalization was not applied to addHeader() and removeHeader(), breaking the round-trip contract: headers stored via addHeader('X-Foo', …) can no longer be retrieved with getHeader('X-Foo') because getHeader will look them up under x-foo.

Confidence Score: 4/5

Safe to merge for the primary Swoole header normalization fix, but the addHeader/removeHeader key inconsistency should be addressed first.

The core TypeError fix is correct and well-targeted. However, the new strtolower in getHeader introduces a regression for any caller using addHeader with non-lowercase keys, since those two methods are now mismatched. This is a P1 concern that warrants a fix before merging.

src/Http/Adapter/Swoole/Request.phpaddHeader and removeHeader need key lowercasing to match getHeader.

Important Files Changed

Filename Overview
src/Http/Adapter/Swoole/Request.php Adds normalizeHeaderValue() and lowercases the key in getHeader() — correctly fixes the TypeError. However, addHeader and removeHeader were not updated to also normalize the key, creating a broken round-trip contract.
tests/SwooleRequestTest.php New regression tests for scalar and array-valued Swoole headers; missing coverage for default-value fallback and case-insensitive key lookup.

Comments Outside Diff (1)

  1. src/Http/Adapter/Swoole/Request.php, line 303-322 (link)

    P1 addHeader / removeHeader not normalized — broken round-trip

    getHeader now lowercases the key before lookup, but addHeader stores the key verbatim. A caller that does addHeader('X-Custom', 'foo') and then calls getHeader('X-Custom') will receive the default value because the stored key X-Custom is never found under its normalized form x-custom. removeHeader has the same asymmetry.

    Both methods should apply strtolower($key) to the key before accessing $this->swoole->header, matching the normalization introduced in getHeader.

Reviews (1): Last reviewed commit: "Normalize array-valued Swoole request he..." | Re-trigger Greptile

Comment on lines +333 to +338
for ($i = count($value) - 1; $i >= 0; $i--) {
$candidate = $value[$i];

if (is_scalar($candidate) || $candidate instanceof Stringable) {
return (string) $candidate;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Numeric-index traversal assumes sequential array

The loop accesses $value[$i] where $i counts down from count($value)-1. If Swoole ever returns an associative (non-sequential) array for a multi-valued header, these accesses will produce Undefined array key warnings in PHP 8 and return null, causing the loop to silently fall through to $default. Using array_values() to re-index first makes this robust:

Suggested change
for ($i = count($value) - 1; $i >= 0; $i--) {
$candidate = $value[$i];
if (is_scalar($candidate) || $candidate instanceof Stringable) {
return (string) $candidate;
}
foreach (array_reverse(array_values($value)) as $candidate) {
if (is_scalar($candidate) || $candidate instanceof Stringable) {
return (string) $candidate;
}
}

Comment on lines +30 to +46
public function testCanGetScalarHeaders(): void
{
$this->request?->getSwooleRequest()->header = [
'x-replaced-path' => '/gateway',
];

$this->assertEquals('/gateway', $this->request?->getHeader('x-replaced-path'));
}

public function testCanNormalizeArrayHeaders(): void
{
$this->request?->getSwooleRequest()->header = [
'x-replaced-path' => ['/client', '/gateway'],
];

$this->assertEquals('/gateway', $this->request?->getHeader('x-replaced-path'));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test cases for default value and case-insensitive lookup

The new tests cover scalar and array values, but two additional cases are worth adding:

  • A missing header should return the provided $default (not '').
  • getHeader should be case-insensitive on the key (getHeader('X-Replaced-Path') vs getHeader('x-replaced-path')).

Without the case-insensitive test, the new strtolower call in getHeader has no coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant