fix: return 401 for non-GET requests instead of 307 redirect to login#526
fix: return 401 for non-GET requests instead of 307 redirect to login#526shafaladhikari wants to merge 4 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR centralizes login redirect behavior: it adds a ChangesLogin Redirect Centralization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/authMiddleware/authMiddleware.ts`:
- Around line 52-64: The loginRedirect function currently returns
NextResponse.json({ statusCode: 401, ... }) which sends HTTP 200; change it to
return NextResponse.json({ statusCode: 401, message: "Unauthorized" }, { status:
401 }) so the transport-level status is correct, and ensure any invitation-code
handling path uses the same loginRedirect flow (or explicitly enforces the
GET/HEAD check and returns the same NextResponse.json(..., { status: 401 }))
instead of directly calling NextResponse.redirect; update references to
loginRedirect and the invitation-code branch so non-GET/HEAD requests always hit
the 401 response.
- Around line 52-64: The invitation-code redirect path lacks the GET/HEAD guard
and so can forward non-GET methods as a 307; update handleInvitationCodeRedirect
to mirror loginRedirect: read req.method (uppercase, default "GET"), and if it's
not "GET" or "HEAD" return a 401 NextResponse.json({ statusCode: 401, message:
"Unauthorized" }); otherwise perform the redirect (or explicitly use a 303/302
redirect) to the registration/login URL; ensure you change only
handleInvitationCodeRedirect (and any callers in handleMiddleware if needed) so
invitation_code redirects cannot replay POSTs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a62922c8-71ea-440e-8595-1a113d46a873
📒 Files selected for processing (1)
src/authMiddleware/authMiddleware.ts
| const loginRedirect = ( | ||
| req, | ||
| loginRedirectUrl: string, | ||
| redirectURLBase: string | undefined, | ||
| ): NextResponse => { | ||
| const method = req.method?.toUpperCase() ?? "GET"; | ||
| if (method !== "GET" && method !== "HEAD") { | ||
| return NextResponse.json({ statusCode: 401, message: "Unauthorized" }); | ||
| } | ||
| return NextResponse.redirect( | ||
| new URL(loginRedirectUrl, redirectURLBase || config.redirectURL), | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Next.js 15 middleware, what HTTP status code does NextResponse.json return by default when no status option is provided?
💡 Result:
In Next.js 15, when you use NextResponse.json without providing a status option, it returns an HTTP 200 status code by default [1][2]. This behavior aligns with the standard Web Response API, which NextResponse extends [3][2].
Citations:
- 1: https://nextjs.org/docs/app/api-reference/functions/next-response
- 2: https://nextjs.im/docs/15/app/api-reference/functions/next-response/
- 3: https://stackoverflow.com/questions/77332669/use-nextresponse-response-or-nextapiresponse-in-returning-get-and-data-in-next
Fix 401 response in loginRedirect (and ensure invitation-code doesn’t bypass it)
loginRedirect returns NextResponse.json(...) without a { status: 401 } option, which results in HTTP 200 by default—so the transport-level 401 contract isn’t met.
- return NextResponse.json({ statusCode: 401, message: "Unauthorized" });
+ return NextResponse.json(
+ { statusCode: 401, message: "Unauthorized" },
+ { status: 401 },
+ );Also, the invitation-code flow still returns NextResponse.redirect(...) directly and appears to bypass the non-GET/HEAD method gate; ensure it goes through the same loginRedirect logic (or otherwise enforces 401 for non-GET/HEAD).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const loginRedirect = ( | |
| req, | |
| loginRedirectUrl: string, | |
| redirectURLBase: string | undefined, | |
| ): NextResponse => { | |
| const method = req.method?.toUpperCase() ?? "GET"; | |
| if (method !== "GET" && method !== "HEAD") { | |
| return NextResponse.json({ statusCode: 401, message: "Unauthorized" }); | |
| } | |
| return NextResponse.redirect( | |
| new URL(loginRedirectUrl, redirectURLBase || config.redirectURL), | |
| ); | |
| }; | |
| const loginRedirect = ( | |
| req, | |
| loginRedirectUrl: string, | |
| redirectURLBase: string | undefined, | |
| ): NextResponse => { | |
| const method = req.method?.toUpperCase() ?? "GET"; | |
| if (method !== "GET" && method !== "HEAD") { | |
| return NextResponse.json( | |
| { statusCode: 401, message: "Unauthorized" }, | |
| { status: 401 }, | |
| ); | |
| } | |
| return NextResponse.redirect( | |
| new URL(loginRedirectUrl, redirectURLBase || config.redirectURL), | |
| ); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/authMiddleware/authMiddleware.ts` around lines 52 - 64, The loginRedirect
function currently returns NextResponse.json({ statusCode: 401, ... }) which
sends HTTP 200; change it to return NextResponse.json({ statusCode: 401,
message: "Unauthorized" }, { status: 401 }) so the transport-level status is
correct, and ensure any invitation-code handling path uses the same
loginRedirect flow (or explicitly enforces the GET/HEAD check and returns the
same NextResponse.json(..., { status: 401 })) instead of directly calling
NextResponse.redirect; update references to loginRedirect and the
invitation-code branch so non-GET/HEAD requests always hit the 401 response.
🧩 Analysis chain
🌐 Web query:
In Next.js middleware, does NextResponse.redirect default to HTTP 307 and preserve the original HTTP method for POST requests?
💡 Result:
Yes. 1) Default status code: In Next.js middleware, NextResponse.redirect defaults to an HTTP 307 Temporary Redirect when you don’t specify a status code. This is shown in the Next.js implementation where it uses init?.status?? 307 [1]. Next.js docs also state that redirect returns a 307 by default [2]. 2) Preservation of method for POST: With an HTTP 307 Temporary Redirect, HTTP semantics require the client to repeat the request with the same method (so a POST stays a POST). MDN describes 307 as “Method and body not changed” [3]. Therefore, when NextResponse.redirect defaults to 307, it preserves the original HTTP method for POST requests in practice/semantics. Note: Next.js redirect helpers can be overridden if you pass a different status (e.g., { status: 308 } for permanent redirect) [1][4], but the default behavior is 307, which preserves method per HTTP semantics [3].
Citations:
- 1: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/response.ts
- 2: https://nextjs.org/docs/app/guides/redirecting
- 3: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections
- 4: https://vercel-next-js.mintlify.app/api-reference/functions/next-response
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/authMiddleware/authMiddleware.ts"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "=== File header/structure ==="
sed -n '1,120p' "$FILE" | nl -ba
echo "=== Search for handleMiddleware and handleInvitationCodeRedirect ==="
rg -n "handleMiddleware|handleInvitationCodeRedirect|handleInvitationCode|invitation_code|loginRedirect" "$FILE" -n
echo "=== Show region around handleMiddleware (lines ~1-220) ==="
sed -n '1,240p' "$FILE" | nl -ba
echo "=== Show region around loginRedirect (lines ~1-120) ==="
rg -n "const loginRedirect" "$FILE"
START=$(rg -n "const loginRedirect" "$FILE" | head -n1 | cut -d: -f1)
START=$((START-30))
END=$((START+60))
sed -n "${START},${END}p" "$FILE" | nl -ba
echo "=== Show region around handleInvitationCodeRedirect (lines ~1-240) ==="
rg -n "const handleInvitationCodeRedirect" "$FILE" || true
START2=$(rg -n "const handleInvitationCodeRedirect" "$FILE" | head -n1 | cut -d: -f1)
START2=$((START2-40))
END2=$((START2+120))
sed -n "${START2},${END2}p" "$FILE" | nl -baRepository: kinde-oss/kinde-auth-nextjs
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/authMiddleware/authMiddleware.ts"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
nl_awk() { awk '{printf "%6d %s\n", NR, $0}'; }
echo "=== File header/structure (first 120 lines) ==="
sed -n '1,120p' "$FILE" | nl_awk
echo "=== Search for handleMiddleware and handleInvitationCodeRedirect ==="
rg -n "handleMiddleware|handleInvitationCodeRedirect|invitation_code|loginRedirect" "$FILE"
echo "=== Region around handleMiddleware (1-240) ==="
sed -n '1,240p' "$FILE" | nl_awk
echo "=== Region around loginRedirect definition ==="
start=$(rg -n "const loginRedirect" "$FILE" | head -n1 | cut -d: -f1)
start=$((start-30))
end=$((start+100))
if [ "$start" -lt 1 ]; then start=1; fi
sed -n "${start},${end}p" "$FILE" | nl_awk
echo "=== Region around handleInvitationCodeRedirect definition ==="
start2=$(rg -n "const handleInvitationCodeRedirect" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -z "${start2}" ]; then
echo "No handleInvitationCodeRedirect definition found"
exit 0
fi
start2=$((start2-40))
end2=$((start2+160))
if [ "$start2" -lt 1 ]; then start2=1; fi
sed -n "${start2},${end2}p" "$FILE" | nl_awkRepository: kinde-oss/kinde-auth-nextjs
Length of output: 25355
Guard invitation-code redirects against non-GET/HEAD (default 307 preserves POST method)
handleMiddleware immediately returns handleInvitationCodeRedirect(...) when invitation_code is present (bypassing loginRedirect(req, ...)), and handleInvitationCodeRedirect performs a bare NextResponse.redirect(...) with no method check. Since NextResponse.redirect defaults to HTTP 307, a POST /protected?invitation_code=... will keep the POST method and be repeated to the register URL (and to the login URL in the error path).
One way to close the gap
-const handleInvitationCodeRedirect = (
+const handleInvitationCodeRedirect = (
+ req,
invitationCode: string,
registerPage: string,
loginRedirectUrl: string,
redirectURLBase: string | undefined,
): NextResponse => {
+ const method = req.method?.toUpperCase() ?? "GET";
+ if (method !== "GET" && method !== "HEAD") {
+ return NextResponse.json(
+ { statusCode: 401, message: "Unauthorized" },
+ { status: 401 },
+ );
+ }
+
try {
const params = new URLSearchParams();
params.set("invitation_code", invitationCode);
@@
return handleInvitationCodeRedirect(
+ req,
invitationCode,
registerPage,
loginRedirectUrl,
options?.redirectURLBase,
);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/authMiddleware/authMiddleware.ts` around lines 52 - 64, The
invitation-code redirect path lacks the GET/HEAD guard and so can forward
non-GET methods as a 307; update handleInvitationCodeRedirect to mirror
loginRedirect: read req.method (uppercase, default "GET"), and if it's not "GET"
or "HEAD" return a 401 NextResponse.json({ statusCode: 401, message:
"Unauthorized" }); otherwise perform the redirect (or explicitly use a 303/302
redirect) to the registration/login URL; ensure you change only
handleInvitationCodeRedirect (and any callers in handleMiddleware if needed) so
invitation_code redirects cannot replay POSTs.
There was a problem hiding this comment.
Fix 401 response in loginRedirect: the finding is technically correct (HTTP 200 with statusCode: 401 in the body is wrong), but fixing it in isolation would make loginRedirect and handleInvitationCodeRedirect diverge from the identical pattern used across all 5 call sites in protect.js. We'd be creating a new inconsistency rather than resolving one.
There was a problem hiding this comment.
@shafaladhikari, that's a fair point — patching just loginRedirect here while leaving the identical { statusCode: 401 } + HTTP 200 pattern intact across protect.js's five call sites would create a worse inconsistency than the one it solves. Deferring to a single coordinated follow-up across both files makes sense.
Would you like me to open a follow-up GitHub issue to track the holistic fix (correcting the HTTP status to 401 across authMiddleware.ts and protect.js)?
✏️ Learnings added
Learnt from: shafaladhikari
Repo: kinde-oss/kinde-auth-nextjs PR: 526
File: src/authMiddleware/authMiddleware.ts:52-64
Timestamp: 2026-06-02T02:42:59.276Z
Learning: In kinde-auth-nextjs, the pattern of returning `NextResponse.json({ statusCode: 401, message: "Unauthorized" })` without `{ status: 401 }` (resulting in HTTP 200 with a 401 status code in the body) is intentionally consistent across `loginRedirect` in `src/authMiddleware/authMiddleware.ts` and five call sites in `protect.js`. The maintainer (shafaladhikari) has explicitly deferred fixing the HTTP transport-level status to a follow-up that will address all locations holistically to avoid introducing new inconsistencies. Do not flag individual instances of this pattern as bugs in isolation.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Explain your changes
Fixes #171
The middleware used
NextResponse.redirect()which defaults to HTTP 307 (preserves request method). When a POST request hit a protected route with an expired session, the middleware redirected it asPOST /api/auth/login, which returned 405 since the handler only exportsGET.Non-GET requests are fetch/XHR calls that cannot meaningfully follow an auth redirect, so the middleware now returns
{ statusCode: 401, message: "Unauthorized" }instead — matching the existing error format used inprotectApi.Checklist
🛟 If you need help, consider asking for advice over in the Kinde community.