Skip to content

Commit bb006e9

Browse files
committed
fix(nextjs): wrap proxy and refresh json() calls in try/catch, replace throw with logout
Three uncaught exceptions that crash middleware with 500: - proxy path: response.json() fired before response.ok check, returns 502 on non-JSON - refresh path: refreshResponse.json() now catches non-JSON bodies and calls logout() - refresh path: missing id_token now calls logout() instead of throwing Also remove duplicate JSDoc paragraph and fix two em-dashes in comments.
1 parent 048987a commit bb006e9

2 files changed

Lines changed: 19 additions & 8 deletions

File tree

src/nextjs/index.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ export type FirebaseJWTPayload = JWTPayload & {
3030
*
3131
* If you want to use a Redis client (ioredis, node-redis, Upstash, etc.) you
3232
* must wrap it in an adapter that maps this interface to the client's own API.
33-
* The options object uses lowercase `ex` / `nx` keys which do NOT match the
34-
* native signatures of ioredis or node-redis v4 — pass them through with the
35-
* appropriate translation in your adapter's `set()` implementation.
36-
*
3733
* The options object uses lowercase `ex` / `nx` keys, which do NOT match the
3834
* native signatures of ioredis or node-redis v4; pass them through with the
3935
* appropriate translation in your adapter's `set()` implementation.
@@ -444,7 +440,13 @@ export async function runMiddleware(
444440

445441
const response = await fetch(url, { method, body, headers });
446442

447-
const json = (await response.json()) as TokenResponse | SignInResponse;
443+
let json: TokenResponse | SignInResponse;
444+
try {
445+
json = (await response.json()) as TokenResponse | SignInResponse;
446+
} catch {
447+
console.error("Proxy response was not JSON:", response.status, response.statusText);
448+
return [new NextResponse("Bad gateway: non-JSON response from Firebase", { status: 502 })];
449+
}
448450
const status = response.status;
449451
const statusText = response.statusText;
450452
if (!response.ok) {
@@ -549,10 +551,19 @@ export async function runMiddleware(
549551
console.error(refreshUrl.origin + refreshUrl.pathname, refreshResponse.status, refreshResponse.statusText);
550552
return logout();
551553
}
552-
const json = (await refreshResponse.json()) as TokenResponse;
554+
let json: TokenResponse;
555+
try {
556+
json = (await refreshResponse.json()) as TokenResponse;
557+
} catch {
558+
console.error("Refresh response was not JSON:", refreshResponse.status, refreshResponse.statusText);
559+
return logout();
560+
}
553561
const newRefreshToken = json.refresh_token;
554562
const newIdToken = json.id_token;
555-
if (!newIdToken) throw new Error("Missing id_token in refresh response");
563+
if (!newIdToken) {
564+
console.error("Missing id_token in refresh response");
565+
return logout();
566+
}
556567
// Full signature + claims verification on the refreshed token. Caching is
557568
// handled inside verifyFirebaseIdToken, so no separate cacheSetEx needed.
558569
const [verifiedNewPayload] = await verifyFirebaseIdToken(

src/nextjs/middleware_verify.node.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ describe("verifyFirebaseIdToken", () => {
219219
await verifyFirebaseIdToken(token, mockProjectId, mockTenantId);
220220
const verifyCallsAfterFirst = vi.mocked(jose.jwtVerify).mock.calls.length;
221221

222-
// Second call: served from cache claim must still be present
222+
// Second call: served from cache; claim must still be present
223223
const [result] = await verifyFirebaseIdToken(token, mockProjectId, mockTenantId);
224224
expect(vi.mocked(jose.jwtVerify).mock.calls.length).toBe(verifyCallsAfterFirst);
225225
expect(result?.customRole).toBe(true);

0 commit comments

Comments
 (0)