Skip to content

Instantly share code, notes, and snippets.

@zaru
Created March 21, 2024 05:35
Show Gist options
  • Save zaru/6b40e64138e4237eb4138ecdf6aa63e6 to your computer and use it in GitHub Desktop.
Save zaru/6b40e64138e4237eb4138ecdf6aa63e6 to your computer and use it in GitHub Desktop.
Next.js middleware実装方針比較:素朴なパターンと高階関数パターン
import {
type NextFetchEvent,
type NextRequest,
NextResponse,
} from "next/server";
async function heavyTask() {
console.log("start heavyTask.", new Date());
// await fetch("http://0.0.0.0:9999/heavy.php", { cache: "no-store" });
console.log("done heavyTask.", new Date());
}
function checkIpRestriction(request: NextRequest) {
console.log("checkIpRestriction called");
const ip = request.ip ?? request.headers.get("x-forwarded-for") ?? "";
// const ok = "120.0.0.1";
const ok = "::1";
return ip !== ok;
}
function verifyBasicAuth(request: NextRequest) {
console.log("verifyBasicAuth called");
const basicAuth = request.headers.get("authorization");
if (!basicAuth) return false;
const authValue = basicAuth.split(" ")[1];
const [user, password] = atob(authValue).split(":");
return user === "username" && password === "password";
}
function requireBasicAuth() {
return NextResponse.json(
{ error: "Please enter credentials" },
{
headers: { "WWW-Authenticate": 'Basic realm="Secure Area"' },
status: 401,
},
);
}
function verifyToken(request: NextRequest) {
console.log("verifyToken called");
const basicAuth = request.headers.get("authorization");
if (!basicAuth) return false;
const token = basicAuth.split(" ")[1];
return token === `Bearer ${process.env.AUTH_TOKEN}`;
}
function requireToken() {
return NextResponse.json(
{ error: "Please enter credentials" },
{
status: 401,
},
);
}
function redirectAccountsPath(request: NextRequest) {
const { pathname } = new URL(request.url);
return NextResponse.redirect(
new URL(pathname.replace("/accounts/", "/users/"), request.url),
);
}
function setCacheControl(response: NextResponse) {
response.headers.set(
"cache-control",
"s-maxage=86400, stale-while-revalidate",
);
}
function splitResponse(
request: NextRequest,
cookieKey: string,
pattern: string[],
) {
const selectPath =
request.cookies.get(cookieKey)?.value ||
pattern[Math.floor(Math.random() * pattern.length)];
const response = NextResponse.rewrite(new URL(selectPath, request.url));
response.cookies.set(cookieKey, selectPath);
return response;
}
export async function basicMiddleware(
request: NextRequest,
event: NextFetchEvent,
) {
const response = NextResponse.next();
const { pathname } = new URL(request.url);
console.log("Middleware called", request.method, pathname);
// IP制限
if (checkIpRestriction(request)) {
return NextResponse.json("", { status: 403 });
}
// BASIC認証
if (/^\/basic-auth(\/|$)/.test(pathname) && !verifyBasicAuth(request)) {
return requireBasicAuth();
}
// トークン認証
if (/^\/token-auth(\/|$)/.test(pathname) && !verifyToken(request)) {
return requireToken();
}
// Redirect
if (/^\/accounts(\/|$)/.test(pathname)) {
return redirectAccountsPath(request);
}
// 任意のCache-Controlを設定
if (/^\/users(\/|$)/.test(pathname)) {
setCacheControl(response);
}
// A/Bテスト
if (pathname === "/ab") {
const pattern = ["/ab", "/ab/pattern-b"];
return splitResponse(request, "ab", pattern);
}
event.waitUntil(heavyTask());
return response;
}
import {
type NextFetchEvent,
type NextMiddleware,
type NextRequest,
NextResponse,
} from "next/server";
type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;
export function chain(
functions: MiddlewareFactory[],
index = 0,
): NextMiddleware {
const current = functions[index];
if (current) {
const next = chain(functions, index + 1);
return current(next);
}
return () => {
console.log("chain end, return NextResponse.next()");
return NextResponse.next();
};
}
function restrictIp(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
console.log("Higher Middleware restrictIp called");
const ip = request.ip ?? request.headers.get("x-forwarded-for") ?? "";
// const ok = "120.0.0.1";
const ok = "::1";
if (ip !== ok) {
return NextResponse.json("", { status: 403 });
}
return middleware(request, event);
};
}
function requireBasicAuth(middleware: NextMiddleware) {
const responseBasicAuth = NextResponse.json(
{ error: "Please enter credentials" },
{
headers: { "WWW-Authenticate": 'Basic realm="Secure Area"' },
status: 401,
},
);
return async (request: NextRequest, event: NextFetchEvent) => {
const { pathname } = new URL(request.url);
if (/^\/basic-auth(\/|$)/.test(pathname)) {
console.log("Higher Middleware requireBasicAuth called");
const basicAuth = request.headers.get("authorization");
if (!basicAuth) {
return responseBasicAuth;
}
const authValue = basicAuth.split(" ")[1];
const [user, password] = atob(authValue).split(":");
if (user !== "username" || password !== "password") {
return responseBasicAuth;
}
} else {
console.log("Higher Middleware requireBasicAuth skipped");
}
return middleware(request, event);
};
}
function requireToken(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const { pathname } = new URL(request.url);
if (/^\/token-auth(\/|$)/.test(pathname)) {
console.log("Higher Middleware requireToken called");
const token = request.headers.get("Authorization");
if (!token || token !== `Bearer ${process.env.AUTH_TOKEN}`) {
return NextResponse.json(
{ error: "Please enter credentials" },
{ status: 401 },
);
}
} else {
console.log("Higher Middleware requireToken called");
}
return middleware(request, event);
};
}
function redirectAccountsPath(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const { pathname } = new URL(request.url);
if (/^\/accounts(\/|$)/.test(pathname)) {
console.log("Higher Middleware redirectAccountsPath called");
return NextResponse.redirect(
new URL(pathname.replace("/accounts/", "/users/"), request.url),
);
}
console.log("Higher Middleware redirectAccountsPath skipped");
return middleware(request, event);
};
}
function setCacheControl(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const { pathname } = new URL(request.url);
if (/^\/users(\/|$)/.test(pathname)) {
console.log("Higher Middleware setCacheControl called");
// 通常版と比較すると、この時点でResponse返しちゃうので後続の処理はされない点が違う
const response = NextResponse.next();
response.headers.set(
"cache-control",
"s-maxage=86400, stale-while-revalidate",
);
return response;
}
console.log("Higher Middleware setCacheControl skipped");
return middleware(request, event);
};
}
function abTest(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const { pathname } = new URL(request.url);
if (pathname === "/ab") {
console.log("Higher Middleware abTest called");
const cookieKey = "ab";
const pattern = ["/ab", "/ab/pattern-b"];
const selectPath =
request.cookies.get(cookieKey)?.value ||
pattern[Math.floor(Math.random() * pattern.length)];
const response = NextResponse.rewrite(new URL(selectPath, request.url));
response.cookies.set(cookieKey, selectPath);
return response;
}
console.log("Higher Middleware abTest skipped");
return middleware(request, event);
};
}
function heavyTask(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
console.log("Higher Middleware heavyTask called");
const task = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Higher Middleware heavyTask done");
};
event.waitUntil(task());
return middleware(request, event);
};
}
export const middleware = chain([
heavyTask,
restrictIp,
requireBasicAuth,
requireToken,
redirectAccountsPath,
setCacheControl,
abTest,
]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment