fix: chain plugin onRequest hooks properly

This commit is contained in:
Taesu
2025-12-31 13:37:13 +09:00
parent bb89498d58
commit 04d64822b3
2 changed files with 139 additions and 10 deletions
+128
View File
@@ -5,6 +5,7 @@ import type {
} from "@better-auth/core";
import { createAuthMiddleware } from "@better-auth/core/api";
import { describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../test-utils/test-instance";
import { getEndpoints } from "./index";
describe("getEndpoints", () => {
@@ -55,3 +56,130 @@ describe("getEndpoints", () => {
});
});
});
describe("onRequest chain", () => {
it("should execute all plugins onRequest handlers in chain", async () => {
const onRequestOrder: string[] = [];
const pluginA: BetterAuthPlugin = {
id: "plugin-a",
async onRequest(request, _ctx) {
onRequestOrder.push("plugin-a");
// Return a modified request - this should NOT stop the chain
const newHeaders = new Headers(request.headers);
newHeaders.set("x-plugin-a", "true");
return {
request: new Request(request, { headers: newHeaders }),
};
},
};
const pluginB: BetterAuthPlugin = {
id: "plugin-b",
async onRequest(request, _ctx) {
onRequestOrder.push("plugin-b");
// This should also execute and see the modified request from plugin-a
const newHeaders = new Headers(request.headers);
newHeaders.set("x-plugin-b", "true");
return {
request: new Request(request, { headers: newHeaders }),
};
},
};
const pluginC: BetterAuthPlugin = {
id: "plugin-c",
async onRequest(_request, _ctx) {
onRequestOrder.push("plugin-c");
// Just observe, don't modify
return;
},
};
const { client, testUser } = await getTestInstance({
plugins: [pluginA, pluginB, pluginC],
});
await client.signIn.email({
email: testUser.email,
password: testUser.password,
});
// All three plugins should have their onRequest called
expect(onRequestOrder).toEqual(["plugin-a", "plugin-b", "plugin-c"]);
});
it("should pass modified request from previous plugin to next plugin", async () => {
let pluginBReceivedHeader: string | null = null;
const pluginA: BetterAuthPlugin = {
id: "plugin-a",
async onRequest(request, _ctx) {
const newHeaders = new Headers(request.headers);
newHeaders.set("x-from-plugin-a", "hello");
return {
request: new Request(request, { headers: newHeaders }),
};
},
};
const pluginB: BetterAuthPlugin = {
id: "plugin-b",
async onRequest(request, _ctx) {
// Should receive the header set by plugin-a
pluginBReceivedHeader = request.headers.get("x-from-plugin-a");
return;
},
};
const { client, testUser } = await getTestInstance({
plugins: [pluginA, pluginB],
});
await client.signIn.email({
email: testUser.email,
password: testUser.password,
});
expect(pluginBReceivedHeader).toBe("hello");
});
it("should stop chain when response is returned", async () => {
const onRequestOrder: string[] = [];
const pluginA: BetterAuthPlugin = {
id: "plugin-a",
async onRequest(_request, _ctx) {
onRequestOrder.push("plugin-a");
// Return a response - this SHOULD stop the chain
return {
response: new Response("Blocked by plugin-a", { status: 403 }),
};
},
};
const pluginB: BetterAuthPlugin = {
id: "plugin-b",
async onRequest(_request, _ctx) {
onRequestOrder.push("plugin-b");
return {
response: new Response("ok", { status: 200 }),
};
},
};
const { client, testUser } = await getTestInstance({
plugins: [pluginA, pluginB],
});
const result = await client.signIn.email({
email: testUser.email,
password: testUser.password,
});
// Only plugin-a should execute, plugin-b should NOT execute
expect(onRequestOrder).toEqual(["plugin-a"]);
// Response should be from plugin-a
expect(result.error?.status).toBe(403);
});
});
+11 -10
View File
@@ -284,25 +284,26 @@ export const router = <Option extends BetterAuthOptions>(
if (disabledPaths.includes(normalizedPath)) {
return new Response("Not Found", { status: 404 });
}
let currentRequest = req;
for (const plugin of ctx.options.plugins || []) {
if (plugin.onRequest) {
const response = await plugin.onRequest(req, ctx);
const response = await plugin.onRequest(currentRequest, ctx);
if (response && "response" in response) {
return response.response;
}
if (response && "request" in response) {
const rateLimitResponse = await onRequestRateLimit(
response.request,
ctx,
);
if (rateLimitResponse) {
return rateLimitResponse;
}
return response.request;
currentRequest = response.request;
}
}
}
return onRequestRateLimit(req, ctx);
const rateLimitResponse = await onRequestRateLimit(currentRequest, ctx);
if (rateLimitResponse) {
return rateLimitResponse;
}
return currentRequest;
},
async onResponse(res) {
for (const plugin of ctx.options.plugins || []) {