fix(integrations): migrate cookie integrations to hooks.finally

This commit is contained in:
Taesu
2026-05-09 23:13:26 +09:00
parent 11de596751
commit 177df6afa9
6 changed files with 151 additions and 93 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"better-auth": patch
---
Migrate `nextCookies`, `sveltekitCookies`, and `tanstackStartCookies` (React and Solid) cookie integrations to use `hooks.finally`. Cookies set by plugins running later in the array are now forwarded to the framework cookie store regardless of plugin declaration order. Closes #8911.
@@ -160,4 +160,69 @@ describe("next-js integration", () => {
expect(session).not.toBeNull();
});
/**
* @see https://github.com/better-auth/better-auth/issues/8911
*/
it("forwards cookies set by plugins ordered after nextCookies()", async () => {
const cookiesSet: Array<{ name: string; value: string }> = [];
const cookiesMock = vi.fn(async () => ({
set: vi.fn((name: string, value: string) => {
cookiesSet.push({ name, value });
}),
delete: vi.fn(),
get: vi.fn(),
}));
vi.doMock("next/headers.js", () => ({
cookies: cookiesMock,
headers: vi.fn(async () => new Headers()),
}));
const [{ getTestInstance }, { nextCookies }, { createAuthMiddleware }] =
await Promise.all([
import("../test-utils/test-instance"),
import("./next-js"),
import("@better-auth/core/api"),
]);
const lateCookieName = "late-plugin-cookie";
const lateCookieValue = "late-value";
const lateCookiePlugin = {
id: "late-cookie",
hooks: {
after: [
{
matcher: () => true,
handler: createAuthMiddleware(async (ctx) => {
ctx.setCookie(lateCookieName, lateCookieValue);
}),
},
],
},
};
const { auth, testUser } = await getTestInstance({
plugins: [nextCookies(), lateCookiePlugin],
});
const signInRes = await auth.api.signInEmail({
body: {
email: testUser.email,
password: testUser.password,
},
returnHeaders: true,
});
const setCookieHeaders = signInRes.headers.getSetCookie();
const lateInResponseHeaders = setCookieHeaders.some((c) =>
c.includes(lateCookieName),
);
expect(lateInResponseHeaders).toBe(true);
const lateInCookieStore = cookiesSet.some(
(c) => c.name === lateCookieName && c.value === lateCookieValue,
);
expect(lateInCookieStore).toBe(true);
});
});
@@ -67,50 +67,48 @@ export const nextCookies = () => {
}),
},
],
after: [
finally: [
{
matcher(ctx) {
matcher() {
return true;
},
handler: createAuthMiddleware(async (ctx) => {
const returned = ctx.context.responseHeaders;
if ("_flag" in ctx && ctx._flag === "router") {
return;
}
if (returned instanceof Headers) {
const setCookies = returned?.get("set-cookie");
if (!setCookies) return;
const parsed = parseSetCookieHeader(setCookies);
let cookieHelper: Awaited<
ReturnType<typeof import("next/headers.js").cookies>
>;
try {
const { cookies } = await import("next/headers.js");
cookieHelper = await cookies();
} catch (error) {
if (
error instanceof Error &&
(error.message.startsWith(
"`cookies` was called outside a request scope.",
) ||
error.message.includes("Cannot find module"))
) {
// Monorepo workspaces outside of Next.js hit this path.
// @see https://nextjs.org/docs/messages/next-dynamic-api-wrong-context
return;
}
throw error;
const returned = ctx.context.responseHeaders;
if (!(returned instanceof Headers)) return;
const setCookies = returned.get("set-cookie");
if (!setCookies) return;
const parsed = parseSetCookieHeader(setCookies);
let cookieHelper: Awaited<
ReturnType<typeof import("next/headers.js").cookies>
>;
try {
const { cookies } = await import("next/headers.js");
cookieHelper = await cookies();
} catch (error) {
if (
error instanceof Error &&
(error.message.startsWith(
"`cookies` was called outside a request scope.",
) ||
error.message.includes("Cannot find module"))
) {
// Monorepo workspaces outside of Next.js hit this path.
// @see https://nextjs.org/docs/messages/next-dynamic-api-wrong-context
return;
}
parsed.forEach((value, key) => {
if (!key) return;
try {
cookieHelper.set(key, value.value, toCookieOptions(value));
} catch {
// this will fail if the cookie is being set on server component
}
});
return;
throw error;
}
parsed.forEach((value, key) => {
if (!key) return;
try {
cookieHelper.set(key, value.value, toCookieOptions(value));
} catch {
// this will fail if the cookie is being set on server component
}
});
}),
},
],
@@ -61,32 +61,30 @@ export const sveltekitCookies = (
id: "sveltekit-cookies",
version: PACKAGE_VERSION,
hooks: {
after: [
finally: [
{
matcher() {
return true;
},
handler: createAuthMiddleware(async (ctx) => {
const returned = ctx.context.responseHeaders;
if ("_flag" in ctx && ctx._flag === "router") {
return;
}
if (returned instanceof Headers) {
const setCookies = returned?.get("set-cookie");
if (!setCookies) return;
const event = getRequestEvent();
if (!event) return;
const parsed = parseSetCookieHeader(setCookies);
for (const [name, attributes] of parsed) {
try {
event.cookies.set(name, attributes.value, {
...toCookieOptions(attributes),
path: attributes.path || "/",
});
} catch {
// this will avoid any issue related to already streamed response
}
const returned = ctx.context.responseHeaders;
if (!(returned instanceof Headers)) return;
const setCookies = returned.get("set-cookie");
if (!setCookies) return;
const event = getRequestEvent();
if (!event) return;
const parsed = parseSetCookieHeader(setCookies);
for (const [name, attributes] of parsed) {
try {
event.cookies.set(name, attributes.value, {
...toCookieOptions(attributes),
path: attributes.path || "/",
});
} catch {
// this will avoid any issue related to already streamed response
}
}
}),
@@ -25,33 +25,29 @@ export const tanstackStartCookies = () => {
id: "tanstack-start-cookies-solid",
version: PACKAGE_VERSION,
hooks: {
after: [
finally: [
{
matcher(ctx) {
matcher() {
return true;
},
handler: createAuthMiddleware(async (ctx) => {
const returned = ctx.context.responseHeaders;
if ("_flag" in ctx && ctx._flag === "router") {
return;
}
if (returned instanceof Headers) {
const setCookies = returned?.get("set-cookie");
if (!setCookies) return;
const parsed = parseSetCookieHeader(setCookies);
const { setCookie } = await import(
"@tanstack/solid-start/server"
);
parsed.forEach((value, key) => {
if (!key) return;
try {
setCookie(key, value.value, toCookieOptions(value));
} catch {
// this will fail if the cookie is being set on server component
}
});
return;
}
const returned = ctx.context.responseHeaders;
if (!(returned instanceof Headers)) return;
const setCookies = returned.get("set-cookie");
if (!setCookies) return;
const parsed = parseSetCookieHeader(setCookies);
const { setCookie } = await import("@tanstack/solid-start/server");
parsed.forEach((value, key) => {
if (!key) return;
try {
setCookie(key, value.value, toCookieOptions(value));
} catch {
// this will fail if the cookie is being set on server component
}
});
}),
},
],
@@ -25,33 +25,29 @@ export const tanstackStartCookies = () => {
id: "tanstack-start-cookies",
version: PACKAGE_VERSION,
hooks: {
after: [
finally: [
{
matcher(ctx) {
matcher() {
return true;
},
handler: createAuthMiddleware(async (ctx) => {
const returned = ctx.context.responseHeaders;
if ("_flag" in ctx && ctx._flag === "router") {
return;
}
if (returned instanceof Headers) {
const setCookies = returned?.get("set-cookie");
if (!setCookies) return;
const parsed = parseSetCookieHeader(setCookies);
const { setCookie } = await import(
"@tanstack/react-start/server"
);
parsed.forEach((value, key) => {
if (!key) return;
try {
setCookie(key, value.value, toCookieOptions(value));
} catch {
// this will fail if the cookie is being set on server component
}
});
return;
}
const returned = ctx.context.responseHeaders;
if (!(returned instanceof Headers)) return;
const setCookies = returned.get("set-cookie");
if (!setCookies) return;
const parsed = parseSetCookieHeader(setCookies);
const { setCookie } = await import("@tanstack/react-start/server");
parsed.forEach((value, key) => {
if (!key) return;
try {
setCookie(key, value.value, toCookieOptions(value));
} catch {
// this will fail if the cookie is being set on server component
}
});
}),
},
],