Skip to content

Send repeated response headers as one line so Swoole keeps every value - #22

Merged
adhikjoshi merged 1 commit into
mainfrom
fix/multi-value-response-headers
Oct 15, 2026
Merged

adhikjoshi merged 1 commit into
mainfrom
fix/multi-value-response-headers

Conversation

@adhikjoshi

@adhikjoshi adhikjoshi commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Swoole keeps one value per header name: a second header() call for the same name replaces the first. SwooleClient::sendResponseHeaders() called header() once per value, so any repeated header (Link, Vary, Cache-Control…) reached the client with only its last value.

Production impact (modelslab.com): the app sends 10 Link values (llms.txt, llms-full.txt, api-catalog, OpenAPI, docs, MCP server card, agent skills, status, terms, privacy) plus Vite preload links; responses arrived with only </privacy-policy>; rel="privacy-policy". Cloudflare Early Hints never got a preload link to cache, so all ~42,200 Early Hints lookups a day were cache misses (504s) and no 103 was ever sent.

Fix

Send repeated values as one comma-separated field line (RFC 9110 §5.3). Set-Cookie is excluded — it already goes out one cookie() call per cookie.

Verification

  • Real Swoole server (phpswoole/swoole:php8.3, Swoole 6.3.0RC1, PHP 8.3.35), response with three Link values, a single header and two cookies:
    • before: Link: </privacy-policy>; rel="privacy-policy" (last value only)
    • after: Link: </build/app.css>; rel=preload; as=style, </llms.txt>; rel="llms-txt", </privacy-policy>; rel="privacy-policy"; X-Single and both Set-Cookie lines unchanged
  • tests/Unit/SwooleClientResponseHeadersTest.php (3 tests): repeated values joined into one call, single values unchanged, cookies never joined. Reverting the fix fails the first test.
  • Local suite without the Swoole extension: same result as main (no new errors); full matrix with Swoole runs in CI.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Swoole keeps one value per header name: a second header() call for the same
name replaces the first. sendResponseHeaders() called header() once per value,
so a response with several Link, Vary or Cache-Control values reached the
client with only the last one. In production a page carrying ten Link values
(llms.txt, API catalog, MCP server card, agent skills, Vite preloads...)
arrived with one, and Cloudflare Early Hints never had a preload link to cache.

Repeated values now go out as one comma-separated field line (RFC 9110,
section 5.3). Set-Cookie keeps its own path, one cookie() call per cookie.

Verified on a real Swoole 6.3.0 server (PHP 8.3): before, Link carried only the
last of three values; after, all three, with cookies and single-value headers
unchanged.
@adhikjoshi
adhikjoshi merged commit fc64fbb into main Oct 15, 2026
8 checks passed
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