Skip to content

Commit d588b65

Browse files
peppescgclaude
andcommitted
fix: use 303 See Other for outbound redirects to avoid 405 on POST flow
`NextResponse.redirect()` defaults to 307 (method-preserving). Now that this route also accepts POST (added in the previous commit), a refresh that arrived via POST would have its outbound redirect to /catalog or /signin sent as POST too — both of which only handle GET, producing 405. Switch both the success and failure redirects to 303 See Other, which unconditionally instructs the browser to follow with GET. Caught by Copilot review on the PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9385954 commit d588b65

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

src/app/api/auth/token-refresh/route.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,23 @@ describe("GET /api/auth/token-refresh", () => {
231231
);
232232
});
233233
});
234+
235+
describe("redirect uses 303 See Other (forces GET on follow-up)", () => {
236+
it("success redirect to safeRedirect is 303", async () => {
237+
mockRefreshSuccess();
238+
const response = await GET(
239+
makeRequest(`${INTERNAL_URL}?redirect=%2Fcatalog`),
240+
);
241+
// 303 prevents a POST that arrived at this route (via 307 from a Server
242+
// Action) from being replayed as a POST against /catalog.
243+
expect(response.status).toBe(303);
244+
});
245+
246+
it("failure redirect to /signin is 303", async () => {
247+
vi.spyOn(console, "warn").mockImplementation(() => {});
248+
mockRefreshFailure();
249+
const response = await GET(makeRequest(INTERNAL_URL));
250+
expect(response.status).toBe(303);
251+
});
252+
});
234253
});

src/app/api/auth/token-refresh/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ const BASE_ORIGIN = new URL(BASE_URL).origin;
3030
*
3131
* Both GET and POST are exported: Next.js `redirect()` outside a Server Action uses
3232
* 307 (method-preserving), so a redirect triggered from a Server Component render
33-
* that follows a Server Action POST reaches this route as a POST.
33+
* that follows a Server Action POST reaches this route as a POST. For the same
34+
* reason, the outbound redirects below use 303 See Other (forces the browser to
35+
* follow with GET) — a default 307 would re-POST `/catalog` or `/signin` and 405.
3436
*/
3537
async function handler(request: NextRequest) {
3638
// Validate redirect target to prevent open redirects.
@@ -64,6 +66,7 @@ async function handler(request: NextRequest) {
6466

6567
const redirectResponse = NextResponse.redirect(
6668
new URL(safeRedirect, BASE_URL),
69+
{ status: 303 },
6770
);
6871

6972
// Copy Set-Cookie headers from Better Auth's internal response directly
@@ -88,7 +91,9 @@ async function handler(request: NextRequest) {
8891
async function signOutAndRedirect(
8992
requestHeaders: Headers,
9093
): Promise<NextResponse> {
91-
const response = NextResponse.redirect(new URL("/signin", BASE_URL));
94+
const response = NextResponse.redirect(new URL("/signin", BASE_URL), {
95+
status: 303,
96+
});
9297
try {
9398
const signOutResponse = await auth.api.signOut({
9499
headers: requestHeaders,

0 commit comments

Comments
 (0)