Skip to content

Commit

Permalink
fix(setCookie): properly merge unique set-cookie values
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Feb 22, 2025
1 parent da29b02 commit de37407
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 18 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"destr": "^2.0.3",
"iron-webcrypto": "^1.2.1",
"node-mock-http": "^1.0.0",
"ohash": "^1.1.4",
"radix3": "^1.1.2",
"ufo": "^1.5.4",
"uncrypto": "^0.1.3"
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 38 additions & 14 deletions src/utils/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { parse, serialize } from "cookie-es";
import { objectHash } from "ohash";
import type { CookieSerializeOptions } from "cookie-es";
import type { H3Event } from "../event";
import {
parse as parseCookie,
serialize as serializeCookie,
parseSetCookie,
} from "cookie-es";
import { getDistinctCookieKey } from "./internal/cookie";

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

/**
Expand Down Expand Up @@ -42,20 +46,40 @@ export function setCookie(
event: H3Event,
name: string,
value: string,
serializeOptions?: CookieSerializeOptions,
serializeOptions: CookieSerializeOptions = {},
) {
serializeOptions = { path: "/", ...serializeOptions };
const cookieStr = serialize(name, value, serializeOptions);
let setCookies = event.node.res.getHeader("set-cookie");
if (!Array.isArray(setCookies)) {
setCookies = [setCookies as any];
// Apply default path
if (!serializeOptions.path) {
serializeOptions = { path: "/", ...serializeOptions };
}

const _optionsHash = objectHash(serializeOptions);
setCookies = setCookies.filter((cookieValue: string) => {
return cookieValue && _optionsHash !== objectHash(parse(cookieValue));
});
event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]);
// Serialize cookie
const newCookie = serializeCookie(name, value, serializeOptions);

// Check and add only not any other set-cookie headers already set
// const currentCookies = event.response.headers.getSetCookie();
const currentCookies = splitCookiesString(
event.node.res.getHeader("set-cookie") as string | string[],
);
if (currentCookies.length === 0) {
event.node.res.setHeader("set-cookie", newCookie);
return;
}

// Merge and deduplicate unique set-cookie headers
const newCookieKey = getDistinctCookieKey(name, serializeOptions);
event.node.res.removeHeader("set-cookie");
for (const cookie of currentCookies) {
const _key = getDistinctCookieKey(
cookie.split("=")?.[0],
parseSetCookie(cookie),
);
if (_key === newCookieKey) {
continue;
}
event.node.res.appendHeader("set-cookie", cookie);
}
event.node.res.appendHeader("set-cookie", newCookie);
}

/**
Expand Down
15 changes: 15 additions & 0 deletions src/utils/internal/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { CookieSerializeOptions, SetCookie } from "cookie-es";

export function getDistinctCookieKey(
name: string,
opts: CookieSerializeOptions | SetCookie,
) {
return [
name,
opts.domain || "",
opts.path || "/",
Boolean(opts.secure),
Boolean(opts.httpOnly),
Boolean(opts.sameSite),
].join(";");
}
19 changes: 19 additions & 0 deletions test/cookie.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,24 @@ describe("", () => {
]);
expect(result.text).toBe("200");
});

it("can merge unique cookies", async () => {
app.use(
"/",
eventHandler((event) => {
setCookie(event, "session", "123", { httpOnly: true });
setCookie(event, "session", "123", {
httpOnly: true,
maxAge: 60 * 60 * 24 * 30,
});
return "200";
}),
);
const result = await request.get("/");
expect(result.headers["set-cookie"]).toEqual([
"session=123; Max-Age=2592000; Path=/; HttpOnly",
]);
expect(result.text).toBe("200");
});
});
});

0 comments on commit de37407

Please sign in to comment.