fix: add missing set/delete/clear/size/toString to RequestCookies#517
Conversation
The RequestCookies class in server.ts (used by NextRequest.cookies) was missing mutation methods that @edge-runtime/cookies provides. Middleware that calls cookies.set() or cookies.delete() to forward modified cookies downstream would fail at runtime. - Add set(), delete(), clear(), size, toString() methods - Cache parsed map in constructor instead of re-parsing on every call - Encode values with encodeURIComponent when syncing back to Cookie header - delete() accepts string | string[] and returns boolean | boolean[] matching @edge-runtime/cookies API
commit: |
|
/bigbonk review |
There was a problem hiding this comment.
Good contribution — the missing set/delete/clear/size/toString methods are genuinely needed for middleware cookie forwarding, and the test coverage is solid. A few issues to address, mostly around behavioral divergence from @edge-runtime/cookies:
Behavioral divergence: delete() after deleting all cookies leaves a stale empty header
Upstream @edge-runtime/cookies always calls this._headers.set('cookie', ...) after delete — it never conditionally calls this._headers.delete('cookie'). Your _syncHeader deletes the header when the map is empty, which is fine (arguably better), but the delete() method has a subtle issue: when the array overload deletes the last cookie, it calls _syncHeader() once, which is correct. But this also means clear() and _syncHeader() have two different code paths for removing the header — clear() inlines this._headers.delete('cookie') while _syncHeader() also does it. Consider having clear() delegate to _syncHeader() for consistency.
toString() duplicates _syncHeader() logic
The serialization logic ([...entries].map(…).join(…)) is duplicated between toString() and _syncHeader(). This is a minor DRY issue but worth noting — if the encoding strategy ever changes, both need updating.
Overall this is in good shape. The main actionable item is the clear() consistency point (which is optional but would be cleaner). Tests are thorough.
| } | ||
|
|
||
| toString(): string { | ||
| return [...this._parsed.entries()].map(([n, v]) => `${n}=${encodeURIComponent(v)}`).join("; "); |
There was a problem hiding this comment.
toString() and _syncHeader() share the same serialization logic. Consider extracting a private helper to avoid duplication:
| return [...this._parsed.entries()].map(([n, v]) => `${n}=${encodeURIComponent(v)}`).join("; "); | |
| toString(): string { | |
| return this._serialize(); | |
| } |
Then add a private _serialize() method and call it from both toString() and _syncHeader(). This way if the encoding strategy changes (e.g., you switch to a stringifyCookie-style function), there's only one place to update.
| clear(): this { | ||
| this._parsed.clear(); | ||
| this._headers.delete("cookie"); | ||
| return this; |
There was a problem hiding this comment.
Nit: clear() manually inlines the header deletion (this._headers.delete("cookie")), while _syncHeader() also handles the empty-map case. For consistency, clear() could delegate to _syncHeader() instead:
| return this; | |
| clear(): this { | |
| this._parsed.clear(); | |
| this._syncHeader(); | |
| return this; | |
| } |
This is functionally equivalent but avoids two separate code paths for removing the cookie header when the map is empty.
| return this; | ||
| } | ||
|
|
||
| delete(names: string | string[]): boolean | boolean[] { |
There was a problem hiding this comment.
Worth noting: @edge-runtime/cookies returns boolean | boolean[] from delete() with the same semantics (single → boolean, array → boolean[]). The return type matches upstream, which is good.
However, upstream's delete() always calls _headers.set('cookie', ...) even when the map is empty (resulting in an empty cookie: header), while this implementation calls _headers.delete('cookie') via _syncHeader(). The vinext behavior (removing the header entirely) is arguably more correct, just flagging the difference for awareness.
|
Review posted. Here's a summary of my findings: Overall: Good PR — fills a real gap in the Key observations from comparing against
The caching of the parsed map in the constructor (replacing the |
Address review feedback: - clear() delegates to _syncHeader() instead of inlining header deletion - Extract _serialize() helper shared by toString() and _syncHeader()
|
/bigbonk re-review |
There was a problem hiding this comment.
Good contribution. The implementation matches @edge-runtime/cookies semantics where it matters, the code is clean, and the tests are thorough. I verified against the upstream source — the API surface, encoding strategy, and return types all align.
The previous review's suggestions (have clear() delegate to _syncHeader(), extract _serialize() helper) have already been addressed in this revision, which is nice to see.
One minor nit and one observation below, neither blocking.
| let cookieValue: string; | ||
| if (typeof nameOrOptions === "string") { | ||
| cookieName = nameOrOptions; | ||
| cookieValue = value ?? ""; |
There was a problem hiding this comment.
Nit: when nameOrOptions is a CookieEntry object, the value parameter is ignored. Upstream does the same, but it might be worth a brief doc comment on the signature noting that value is only used when nameOrOptions is a string. Not blocking.
| expect(headers.get("cookie")).toBe("data=hello%3Bworld%3Dfoo"); | ||
| }); | ||
|
|
||
| it("set() preserves existing percent-encoded cookies", async () => { |
There was a problem hiding this comment.
This test checks that the get() value is correct after mutation, but doesn't assert on the serialized header. It might be worth adding an assertion like:
expect(headers.get("cookie")).toContain("token=100%2525done");to verify the header round-trips correctly (the % in 100%done should be re-encoded as %25 by encodeURIComponent). Currently the test only verifies the in-memory parsed value, not the wire format.
Not blocking — the existing coverage is solid.
|
Review submitted. Here's a summary of my findings: Verdict: Approved The PR correctly fills a real gap in the
Minor intentional divergence (not a problem): When all cookies are deleted, this implementation removes the Two non-blocking comments left:
|
Summary
RequestCookiesclass inserver.ts(used byNextRequest.cookiesin middleware) was missingset(),delete(),clear(),size, andtoString()methods that@edge-runtime/cookiesprovidescookies.set()orcookies.delete()to forward modified cookies downstream would fail at runtimeCookieheader on every method callencodeURIComponentwhen syncing back to theCookieheader, preventing round-trip data corruptiondelete()acceptsstring | string[]and returnsboolean | boolean[]matching@edge-runtime/cookiesTest plan
RequestCookies APIdescribe block covering:set()— add, overwrite, object form, chainingdelete()— single name, array of names, return value (boolean/boolean[]), no-op for missingclear()— removes all cookies and clears headersize— count, emptytoString()— serialization;,=,%)set()thendelete()interaction