Skip to content

Commit d1c00fc

Browse files
authored
fix(setCookie): properly merge and dedup set-cookie header (#981)
1 parent da29b02 commit d1c00fc

File tree

6 files changed

+71
-19
lines changed

6 files changed

+71
-19
lines changed

docs/2.utils/98.advanced.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Get a cookie value by name.
5252

5353
Parse the request to get HTTP Cookie header string and return an object of all cookie name-value pairs.
5454

55-
### `setCookie(event, name, value, serializeOptions?)`
55+
### `setCookie(event, name, value, serializeOptions)`
5656

5757
Set a cookie value by name.
5858

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"destr": "^2.0.3",
4040
"iron-webcrypto": "^1.2.1",
4141
"node-mock-http": "^1.0.0",
42-
"ohash": "^1.1.4",
4342
"radix3": "^1.1.2",
4443
"ufo": "^1.5.4",
4544
"uncrypto": "^0.1.3"

pnpm-lock.yaml

-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/utils/cookie.ts

+36-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { parse, serialize } from "cookie-es";
2-
import { objectHash } from "ohash";
31
import type { CookieSerializeOptions } from "cookie-es";
42
import type { H3Event } from "../event";
3+
import {
4+
parse as parseCookie,
5+
serialize as serializeCookie,
6+
parseSetCookie,
7+
} from "cookie-es";
8+
import { getDistinctCookieKey } from "./internal/cookie";
59

610
/**
711
* Parse the request to get HTTP Cookie header string and return an object of all cookie name-value pairs.
@@ -12,7 +16,7 @@ import type { H3Event } from "../event";
1216
* ```
1317
*/
1418
export function parseCookies(event: H3Event): Record<string, string> {
15-
return parse(event.node.req.headers.cookie || "");
19+
return parseCookie(event.node.req.headers.cookie || "");
1620
}
1721

1822
/**
@@ -42,20 +46,38 @@ export function setCookie(
4246
event: H3Event,
4347
name: string,
4448
value: string,
45-
serializeOptions?: CookieSerializeOptions,
49+
serializeOptions: CookieSerializeOptions = {},
4650
) {
47-
serializeOptions = { path: "/", ...serializeOptions };
48-
const cookieStr = serialize(name, value, serializeOptions);
49-
let setCookies = event.node.res.getHeader("set-cookie");
50-
if (!Array.isArray(setCookies)) {
51-
setCookies = [setCookies as any];
51+
// Apply default path
52+
if (!serializeOptions.path) {
53+
serializeOptions = { path: "/", ...serializeOptions };
5254
}
5355

54-
const _optionsHash = objectHash(serializeOptions);
55-
setCookies = setCookies.filter((cookieValue: string) => {
56-
return cookieValue && _optionsHash !== objectHash(parse(cookieValue));
57-
});
58-
event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]);
56+
// Serialize cookie
57+
const newCookie = serializeCookie(name, value, serializeOptions);
58+
59+
// Check and add only not any other set-cookie headers already set
60+
// const currentCookies = event.response.headers.getSetCookie();
61+
const currentCookies = splitCookiesString(
62+
event.node.res.getHeader("set-cookie") as string | string[],
63+
);
64+
if (currentCookies.length === 0) {
65+
event.node.res.setHeader("set-cookie", newCookie);
66+
return;
67+
}
68+
69+
// Merge and deduplicate unique set-cookie headers
70+
const newCookieKey = getDistinctCookieKey(name, serializeOptions);
71+
event.node.res.removeHeader("set-cookie");
72+
for (const cookie of currentCookies) {
73+
const parsed = parseSetCookie(cookie);
74+
const key = getDistinctCookieKey(parsed.name, parsed);
75+
if (key === newCookieKey) {
76+
continue;
77+
}
78+
event.node.res.appendHeader("set-cookie", cookie);
79+
}
80+
event.node.res.appendHeader("set-cookie", newCookie);
5981
}
6082

6183
/**

src/utils/internal/cookie.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { CookieSerializeOptions, SetCookie } from "cookie-es";
2+
3+
export function getDistinctCookieKey(
4+
name: string,
5+
opts: CookieSerializeOptions | SetCookie,
6+
) {
7+
return [
8+
name,
9+
opts.domain || "",
10+
opts.path || "/",
11+
Boolean(opts.secure),
12+
Boolean(opts.httpOnly),
13+
Boolean(opts.sameSite),
14+
].join(";");
15+
}

test/cookie.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,24 @@ describe("", () => {
101101
]);
102102
expect(result.text).toBe("200");
103103
});
104+
105+
it("can merge unique cookies", async () => {
106+
app.use(
107+
"/",
108+
eventHandler((event) => {
109+
setCookie(event, "session", "123", { httpOnly: true });
110+
setCookie(event, "session", "123", {
111+
httpOnly: true,
112+
maxAge: 60 * 60 * 24 * 30,
113+
});
114+
return "200";
115+
}),
116+
);
117+
const result = await request.get("/");
118+
expect(result.headers["set-cookie"]).toEqual([
119+
"session=123; Max-Age=2592000; Path=/; HttpOnly",
120+
]);
121+
expect(result.text).toBe("200");
122+
});
104123
});
105124
});

0 commit comments

Comments
 (0)