Skip to content

perf(cookie): avoid quadratic chunked cookie parsing and header rebuilds#1368

Closed
trivikr wants to merge 3 commits into
h3js:mainfrom
trivikr:chunked-cookie-handling
Closed

perf(cookie): avoid quadratic chunked cookie parsing and header rebuilds#1368
trivikr wants to merge 3 commits into
h3js:mainfrom
trivikr:chunked-cookie-handling

Conversation

@trivikr

@trivikr trivikr commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR improves cookie performance in the chunked-cookie path used by sessions.

Previously, setCookie() rebuilt and reparsed the full set-cookie header list on every write. Since setChunkedCookie() calls setCookie() for the main cookie and for each chunk, large chunked session writes became quadratic in chunk count. The read path had a similar issue: getChunkedCookie() repeatedly called getCookie(), which reparsed the full request cookie header each time.

What Changed

  • Cache parsed request cookies per Headers instance in src/utils/cookie.ts
  • Update getChunkedCookie() to read from a single parsed cookie map instead of reparsing per chunk.
  • Keep response-side set-cookie state in memory so unique cookie writes can append without reparsing existing headers.
  • Preserve deduplication behavior for overwrites by rebuilding headers only when the same logical cookie key is replaced.

Summary by CodeRabbit

  • Refactor
    • Improved cookie handling performance by caching parsed request cookies and response Set-Cookie state to reduce repeated parsing.
    • Optimized chunked cookie reads to parse the request cookie header once.
    • Updated Set-Cookie behavior to update headers incrementally when possible.
  • Tests
    • Added performance-focused unit tests for chunked cookie utilities (single parse on read; no extra parsing when appending).
    • Extended coverage to ensure parse results aren’t affected by mutations of returned cookie objects and that externally added Set-Cookie values aren’t removed by later calls.

Assisted-by: OpenAI:gpt-5.4
Signed-off-by: Kamat, Trivikram <[email protected]>
@trivikr
trivikr requested a review from pi0 as a code owner April 16, 2026 14:09
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Refactors cookie parsing and Set-Cookie writing in src/utils/cookie.ts to reuse cached header state, reduces repeated parsing in chunked cookie reads, and adds tests for cookie immutability and preserving externally added response headers.

Changes

Cookie cache and chunked utility updates

Layer / File(s) Summary
Request cookie memoization
src/utils/cookie.ts
Adds WeakMap-backed request cookie caching keyed by the live Headers object and current "cookie" string, and updates chunked cookie reads to use one parsed cookie map.
Response Set-Cookie state updates
src/utils/cookie.ts
Changes setCookie to use cached response SetCookieState, append or replace entries by distinct key, and write updated "set-cookie" headers through internal helpers.
Cookie utility tests
test/cookies.test.ts
Adds assertions that parsed cookies remain isolated across reads and that externally appended "set-cookie" headers are preserved on later writes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: pi0

Poem

🐇 I found a cookie trail so neat,
A cache for crumbs, a faster beat.
One hop to parse, one hop to write,
And headers stay in tidy sight. 🍪

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main performance-focused cookie changes in chunked parsing and header rebuilding.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/utils/cookie.ts (1)

313-321: Malformed cookie handling is reasonable but worth documenting.

When parseSetCookie returns undefined for a malformed cookie (Line 316), the cookie is preserved in state.cookies but its key is undefined, meaning it won't match for deduplication. This is a reasonable fallback—malformed cookies won't be lost, but repeated writes of the same malformed cookie would accumulate. Consider adding a brief comment explaining this behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/cookie.ts` around lines 313 - 321, The code intentionally preserves
malformed Set-Cookie strings in state.cookies while pushing undefined into
state.keys when parseSetCookie returns undefined, which prevents them from being
deduplicated via state.distinctKeys; add a concise inline comment next to the
loop (or above the parseSetCookie usage) explaining that malformed cookies are
retained unchanged, that _getDistinctCookieKey is only called for parsed
cookies, and that resulting undefined keys mean malformed cookies will not be
deduplicated (so repeated malformed cookies may accumulate), to make this
fallback behavior explicit for future readers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils/cookie.ts`:
- Around line 313-321: The code intentionally preserves malformed Set-Cookie
strings in state.cookies while pushing undefined into state.keys when
parseSetCookie returns undefined, which prevents them from being deduplicated
via state.distinctKeys; add a concise inline comment next to the loop (or above
the parseSetCookie usage) explaining that malformed cookies are retained
unchanged, that _getDistinctCookieKey is only called for parsed cookies, and
that resulting undefined keys mean malformed cookies will not be deduplicated
(so repeated malformed cookies may accumulate), to make this fallback behavior
explicit for future readers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3dcf89a2-5780-4843-9557-295def9802b7

📥 Commits

Reviewing files that changed from the base of the PR and between e2f8cbd and 03665bb.

📒 Files selected for processing (2)
  • src/utils/cookie.ts
  • test/unit/cookie-performance.test.ts

@pkg-pr-new

pkg-pr-new Bot commented Apr 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/h3@1368

commit: 03665bb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/cookie.ts (1)

297-345: Consider extracting _getSetCookieState / _writeSetCookieState to an internal module.

This file is now well beyond the short-file target; moving this helper block to src/utils/internal/ would keep cookie.ts easier to maintain.

As per coding guidelines, "Keep files short — aim for less than 200 lines of code per file, split when larger."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/cookie.ts` around lines 297 - 345, Extract the helper functions
_getSetCookieState and _writeSetCookieState into a new internal module under
src/utils/internal (e.g., src/utils/internal/setCookieState.ts), move any
related types/consts they need (SetCookieState, responseCookiesCache) or
re-export them as needed, and update all callers in cookie.ts to import these
functions from the new module; ensure references to parseSetCookie,
_getDistinctCookieKey, and Headers remain accessible (either by importing them
into the new module or keeping them in cookie.ts and passing them in), run/fix
tests and exports so behavior is identical after the move.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/cookie.ts`:
- Around line 34-44: The parseCookies function currently returns the
cached.cookies object instance directly from requestCookiesCache, allowing
callers to mutate shared state; change parseCookies (and the cache set) to
always return a shallow cloned cookies object instead of the original
reference—when reading cached (cached.source and cached.cookies) or after
computing cookies via parseCookie(source), return a new object (e.g.,
{...cached.cookies} or a new object copy of parseCookie result) so callers
cannot mutate the cache stored in requestCookiesCache; keep using the same keys
(headers, source) and function names (parseCookies, parseCookie,
requestCookiesCache) but ensure clones are stored/returned rather than the
original object references.

---

Nitpick comments:
In `@src/utils/cookie.ts`:
- Around line 297-345: Extract the helper functions _getSetCookieState and
_writeSetCookieState into a new internal module under src/utils/internal (e.g.,
src/utils/internal/setCookieState.ts), move any related types/consts they need
(SetCookieState, responseCookiesCache) or re-export them as needed, and update
all callers in cookie.ts to import these functions from the new module; ensure
references to parseSetCookie, _getDistinctCookieKey, and Headers remain
accessible (either by importing them into the new module or keeping them in
cookie.ts and passing them in), run/fix tests and exports so behavior is
identical after the move.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4fc46945-ade0-45f8-8171-adbbe74d3ede

📥 Commits

Reviewing files that changed from the base of the PR and between 03665bb and 1b53a6f.

📒 Files selected for processing (1)
  • src/utils/cookie.ts

Comment thread src/utils/cookie.ts Outdated
@pi0x

pi0x commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Reviewed this branch and found two correctness issues in the new caching, both now fixed with regression tests (on the integration branch) — the core optimization is preserved (0 parseSetCookie on the append path, 1 parseCookie per read).

1. set-cookie written outside setCookie is silently dropped (data loss). Once setCookie ran, the SetCookieState WeakMap became the sole source of truth. Any set-cookie appended to event.res.headers elsewhere (middleware, proxy, direct append) was invisible, and the dedup path rebuilt the header list from cached state only — dropping the external cookie on the next overwrite.

setCookie(event, "a", "1");
event.res.headers.append("set-cookie", "x=9"); // external writer
setCookie(event, "a", "2");                     // overwrite
// this branch: [ 'a=2; Path=/' ]         ← x=9 lost
// main:        [ 'x=9', 'a=2; Path=/' ]   ← preserved

Fix: _getSetCookieState reconciles the cache against live getSetCookie() each call — reused untouched on the all-through-h3 path (still zero parseSetCookie), rebuilt (re-parsing only unknown cookies) when headers diverge.

2. parseCookies returned the shared cached object. Previously each call returned a fresh record; callers mutating the result now corrupted the memoized parse for every subsequent read on that request. Fix: return a shallow copy (negligible vs. re-parsing).

Minor/no-fix: unparseable set-cookie entries are now preserved rather than dropped during dedup — a behavior change from main but a no-data-loss improvement; worth a note in the description.

Added regression tests for both (external-append-survives-overwrite; mutation-doesn't-corrupt-cache) — both fail before the fix, pass after; full cookie + perf suites green.


This review and the accompanying fixes were generated with AI assistance (Claude) and reviewed by a human before posting.

…cookies

Reconcile the cached set-cookie state against live headers on each access so
set-cookie entries written outside setCookie (middleware, proxy, direct
append) are no longer silently dropped when an h3-managed cookie is
overwritten. The cache is reused untouched on the common all-through-h3 path
(zero parseSetCookie calls preserved); it is rebuilt only when headers diverge.

Also return a shallow copy from parseCookies so mutating the result no longer
corrupts the memoized parse for subsequent reads.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/cookies.test.ts (1)

149-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding coverage for the unparseable-cookie preservation change.

PR notes call out that unparseable set-cookie entries are now preserved during dedup instead of dropped — a deliberate behavior change from main. No test here exercises that path (e.g., appending a malformed set-cookie string externally, then overwriting an h3-managed cookie with the same key, and asserting the malformed entry survives).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cookies.test.ts` around lines 149 - 162, Add coverage for the new
unparseable set-cookie preservation behavior in cookies.test.ts. Extend the
existing set-cookie dedup test around setCookie and res.headers.append so it
includes a malformed external set-cookie entry, then overwrite the h3-managed
cookie with the same key and assert the malformed entry is still preserved in
getSetCookie(). Use the existing setCookie helper and the same app route
structure so the test targets the dedup logic change directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/cookies.test.ts`:
- Around line 149-162: Add coverage for the new unparseable set-cookie
preservation behavior in cookies.test.ts. Extend the existing set-cookie dedup
test around setCookie and res.headers.append so it includes a malformed external
set-cookie entry, then overwrite the h3-managed cookie with the same key and
assert the malformed entry is still preserved in getSetCookie(). Use the
existing setCookie helper and the same app route structure so the test targets
the dedup logic change directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59f176a9-3f44-459b-bf67-3a24cc8b801c

📥 Commits

Reviewing files that changed from the base of the PR and between 1b53a6f and d2991ad.

📒 Files selected for processing (2)
  • src/utils/cookie.ts
  • test/cookies.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/cookie.ts

@pi0

pi0 commented Jul 15, 2026

Copy link
Copy Markdown
Member

Thanks for this PR! Landing via #1472 alternatively with less changes.

@pi0 pi0 closed this Jul 15, 2026
@trivikr
trivikr deleted the chunked-cookie-handling branch July 15, 2026 14:35
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.

3 participants