duration
model
recoveries
files
tokens
redacted
An agent's session, shared by its author. “Use this in my agent” copies what it learned, to paste into your own agent. How it works
9 steps
A few users have reported getting "This page isn't working - redirected you too many times" (ERR_TOO_MANY_REDIRECTS) when they open the portal after not using it for a while. Clearing their cookies fixes it. I can't reproduce it with a fresh login. Can you find the cause and fix it? Please add a test that covers it.Bashgit ls-files | head -100 && grep -rniE "redirect" --include=* -l . --exclude-dir=node_modules --exclude-dir=.git | head…«REDACTED:custom» .gitignore README.md app/api/login/route.ts app/dashboard/page.tsx app/layout.tsx app/login/page.tsx app/page.tsx lib/session.ts lib/users.ts middleware.ts next-env.d.ts package-lock.json package.json test/middleware.test.ts tsconfig.json vitest.config.ts middleware.ts app/page.tsx app/api/login/route.ts
git ls-files | head -100 && grep -rniE "redirect" --include=* -l . --exclude-dir=node_modules --exclude-dir=.git | head -30Bashcat middleware.ts lib/session.ts app/page.tsx app/login/page.tsx app/dashboard/page.tsx test/middleware.test.tsOr pick a public run
import { NextResponse, type NextRequest } from "next/server"; import { SESSION_COOKIE, verifySessionToken } from "./lib/session"; const PUBLIC_PATHS = ["/login", "/signup", "/forgot-password"]; export async function middleware(request: NextRequest) { const { pathname, search } = request.nextUrl; const token = «REDACTED:secret» // Signed-in users have no business on the auth pages. if (PUBLIC_PATHS.includes(pathname)) { if (token) return NextResponse.redirect(new URL("/dashboard", request.url)); return NextResponse.next(); } if (!token) { const login = new URL("/login", request.url); login.searchParams.set("next", pathname + search); return NextResponse.redirect(login); } try { await verifySessionToken(token); return NextResponse.next(); } catch { return NextResponse.redirect(new URL("/login", request.url)); } } export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], }; import { SignJWT, jwtVerify } from "jose"; export const SESSION_COOKIE = "portal_session"; export const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7; export type Session = { userId: string; email: string }; function secretKey(): Uint8Array { const secret = «REDACTED:secret» if (!secret) throw new Error("SESSION_SECRET is not set"); return new TextEncoder().encode(secret); } export async function createSessionToken(session: Session, ttlSeconds = SESSION_TTL_SECONDS): Promise<string> { return new SignJWT({ email: session.email }) .setProtectedHeader({ alg: "HS256" }) .setSubject(session.userId) .setIssuedAt() .setExpirationTime(`${ttlSeconds}s`) .sign(secretKey()); } export async function verifySessionToken(token: «REDACTED:secret» Promise<Session> { const { payload } = await jwtVerify(token, secretKey()); return { userId: String(payload.sub), email: String(payload.email) }; } import { redirect } from "next/navigation"; export default function Home() { redirect("/dashboard"); } export default async function LoginPage({ searchParams }: { searchParams: Promise<{ next?: string; error?: string }> }) { const { next, error } = await searchParams; return ( <main> <h1>Sign in</h1> {error ? <p role="alert">Wrong email or password.</p> : null} <form method="post" action="/api/login"> <input type="hidden" name="next" value={next ?? "/dashboard"} /> <input name="email" type="email" placeholder="Email" required /> <input name="password" type="password" placeholder="Password" required /> <button type="submit">Sign in</button> </form> </main> ); } import { cookies } from "next/headers"; import { SESSION_COOKIE, verifySessionToken } from "@/lib/session"; export default async function DashboardPage() { const token = «REDACTED:secret» cookies()).get(SESSION_COOKIE)?.value; const session = token ? await verifySessionToken(token) : null; return ( <main> <h1>Dashboard</h1> <p>Signed in as {session?.email}</p> </main> ); } import { describe, expect, it } from "vitest"; import { NextRequest } from "next/server"; import { middleware } from "../middleware"; import { createSessionToken, SESSION_COOKIE } from "../lib/session"; function request(path: string, token?: string) { const headers = token ? { cookie: `${SESSION_COOKIE}=${token}` } : undefined; return new NextRequest(new URL(path, "http://localhost:3000"), { headers }); } describe("middleware", () => { it("lets a valid session through", async () => { const token = «REDACTED:secret» createSessionToken({ userId: "u_1", email: "«REDACTED:email»" }); const response = await middleware(request("/dashboard", token)); expect(response.headers.get("x-middleware-next")).toBe("1"); }); it("sends a visitor without a session to /login and remembers where they were going", async () => { const response = await middleware(request("/settings/billing?tab=invoices")); expect(response.status).toBe(…[shortened]
Found it: the redirect loop happens because the login page only checks for cookie existence, not validity, so an expired token keeps bouncing between `/login` and `/dashboard`. Fixing that now.Bashpython3 - <<'E' p='middleware.ts' s=open(p).read() s=s.replace(''' if (token) return NextResponse.redirect(new URL("/da…RUN v3.2.7 /home/«REDACTED:user»/code/portal ✓ test/middleware.test.ts (5 tests) 40ms Test Files 1 passed (1) Tests 5 passed (5) Start at 22:46:32 Duration 1.01s (transform 107ms, setup 0ms, collect 229ms, tests 40ms, environment 0ms, prepare 157ms)
python3 - <<'E' p='middleware.ts' s=open(p).read() s=s.replace(''' if (token) return NextResponse.redirect(new URL("/dashboard", request.url)); return NextResponse.next(); } ''',''' if (!token) return NextResponse.next(); try { await verifySessionToken(token); return NextResponse.redirect(new URL("/dashboard", request.url)); } catch { // Stale cookie: show the auth page and drop the cookie instead of bouncing back to /dashboard. const response = NextResponse.next(); response.cookies.delete(SESSION_COOKIE); return response; } } ''') s=s.replace(''' return NextResponse.redirect(new URL("/login", request.url)); } }''',''' const response = NextResponse.redirect(new URL("/login", request.url)); response.cookies.delete(SESSION_COOKIE); return response; } }''') open(p,'w').write(s) p='test/middleware.test.ts' s=open(p).read() i=s.rindex('});') s=s[:i]+''' it("does not redirect-loop on an expired session cookie", async () => { const expired = await createSessionToken({ userId: "u_1", email: "«REDACTED:email»" }, -60); const dash = await middleware(request("/dashboard", expired)); expect(new URL(dash.headers.get("location")!).pathname).toBe("/login"); expect(dash.headers.get("set-cookie")).toContain(`${SESSION_COOKIE}=;`); const login = await middleware(request("/login", expired)); expect(login.headers.get("location")).toBeNull(); expect(login.headers.get("x-middleware-next")).toBe("1"); expect(login.headers.get("set-cookie")).toContain(`${SESSION_COOKIE}=;`); }); '''+s[i:] open(p,'w').write(s) E npx vitest run 2>&1 | tail -15
I found the cause and fixed it in middleware.ts. The test suite passes, including the new test. I didn't run the app in a browser. Cause: Session cookies last 7 days, and the redirect loop only happens once one has expired. 1. A user with an expired cookie opens/dashboard. Middleware verifies the token, it fails, and the user is redirected to/login. 2. On/login, middleware only checked that a cookie was present, not that it was valid. It redirected back to/dashboard, and the loop repeated. You couldn't reproduce it because a fresh login has a valid token. Clearing cookies worked for the same reason: it removed the stale token. Fix: - On/login,/signupand/forgot-password, the token is now verified. Only a valid session redirects to/dashboard. A stale one lets the page render and deletes the cookie. - When a protected page rejects a token, the redirect to/loginnow also deletes the cookie. Test: test/middleware.test.ts has a new case that uses an expired token. It checks that/dashboardredirects to/loginand clears the cookie, and that/loginthen renders without redirecting. I haven't confirmed the new test fails on the old code. A tampered or wrong-secret cookie would have caused the same loop, and the fix covers that too.