Created
March 21, 2024 05:35
-
-
Save zaru/6b40e64138e4237eb4138ecdf6aa63e6 to your computer and use it in GitHub Desktop.
Next.js middleware実装方針比較:素朴なパターンと高階関数パターン
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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