diff --git a/packages/api/src/lib/vercel.ts b/packages/api/src/lib/vercel.ts index a380f38c..c4dddcd3 100644 --- a/packages/api/src/lib/vercel.ts +++ b/packages/api/src/lib/vercel.ts @@ -17,6 +17,20 @@ export async function vercelFetch(path: string, init?: RequestInit) { }); } +// Vercel can return non-JSON bodies (plain-text error pages during outages, +// rate limits) — never let JSON.parse bubble up as an INTERNAL_SERVER_ERROR. +export async function parseVercelJson(response: Response): Promise { + try { + return await response.json(); + } catch { + console.error("Vercel returned a non-JSON response:", { + status: response.status, + url: response.url, + }); + return null; + } +} + export async function addDomainToVercel(domain: string) { const response = await vercelFetch( `/v9/projects/${env.PROJECT_ID_VERCEL}/domains?teamId=${env.TEAM_ID_VERCEL}`, diff --git a/packages/api/src/router/domain.ts b/packages/api/src/router/domain.ts index 6b8d2773..4e083312 100644 --- a/packages/api/src/router/domain.ts +++ b/packages/api/src/router/domain.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { env } from "../env"; -import { vercelFetch } from "../lib/vercel"; +import { parseVercelJson, vercelFetch } from "../lib/vercel"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const domainConfigResponseSchema = z.object({ @@ -41,6 +41,15 @@ export const domainResponseSchema = z.object({ .optional(), }); +const domainResponseWithErrorSchema = domainResponseSchema.extend({ + error: z + .object({ + code: z.string(), + message: z.string(), + }) + .optional(), +}); + export type DomainVerificationResponse = z.infer; export type DomainConfigResponse = z.infer; export type DomainResponse = z.infer; @@ -61,17 +70,13 @@ export const domainRouter = createTRPCRouter({ const data = await vercelFetch( `/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${opts.input.domain}?teamId=${env.TEAM_ID_VERCEL}`, ); - const json = await data.json(); - const result = domainResponseSchema - .extend({ - error: z - .object({ - code: z.string(), - message: z.string(), - }) - .optional(), - }) - .parse(json); + const json = await parseVercelJson(data); + if (json === null) { + return domainResponseWithErrorSchema.parse({ + error: { code: "internal_error", message: "Unexpected response" }, + }); + } + const result = domainResponseWithErrorSchema.parse(json); console.log({ result }); return result; }), @@ -84,7 +89,10 @@ export const domainRouter = createTRPCRouter({ const data = await vercelFetch( `/v6/domains/${opts.input.domain}/config?teamId=${env.TEAM_ID_VERCEL}`, ); - const json = await data.json(); + const json = await parseVercelJson(data); + if (json === null) { + return null; + } const result = domainConfigResponseSchema.parse(json); return result; }), @@ -98,7 +106,10 @@ export const domainRouter = createTRPCRouter({ `/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${opts.input.domain}/verify?teamId=${env.TEAM_ID_VERCEL}`, { method: "POST" }, ); - const json = await data.json(); + const json = await parseVercelJson(data); + if (json === null) { + return null; + } const result = domainResponseSchema.parse(json); return result; }),