nexu-io/open-design · error · DeployError

Vercel returned a non-JSON response.

Error message

Vercel returned a non-JSON response.

What it means

readVercelJson mirrors the Cloudflare pattern: resp.json() is called on every Vercel API response, and a parse failure is wrapped as a DeployError using the HTTP status (or 502 fallback). It means Vercel returned HTML, empty, or plain-text instead of the expected JSON body.

Source

Thrown at apps/daemon/src/deploy.ts:1907

  if (Number.isFinite(totalPages) && totalPages > 0) return page < totalPages;
  const totalCount = Number(resultInfo?.total_count);
  const responsePerPage = Number(resultInfo?.per_page);
  const effectivePerPage = Number.isFinite(responsePerPage) && responsePerPage > 0
    ? responsePerPage
    : perPage;
  if (Number.isFinite(totalCount) && totalCount >= 0) {
    return page * effectivePerPage < totalCount;
  }
  const count = Number(resultInfo?.count);
  if (Number.isFinite(count) && count >= 0) return count >= effectivePerPage;
  return itemCount >= perPage;
}

async function readVercelJson(resp: Response): Promise<JsonObject> {
  try {
    return await resp.json() as JsonObject;
  } catch {
    throw new DeployError('Vercel returned a non-JSON response.', resp.status || 502);
  }
}

function cloudflareError(json: JsonObject, status: number, fallback: string) {
  const message =
    json?.errors?.find?.((err: JsonObject) => err?.message)?.message ||
    json?.messages?.find?.((item: JsonObject) => item?.message)?.message ||
    json?.message ||
    fallback ||
    `Cloudflare request failed (${status}).`;
  return new DeployError(message, status, json);
}

function isCloudflareAlreadyExists(body: unknown) {
  const text = JSON.stringify(body || {}).toLowerCase();
  return (
    text.includes('already exists') ||
    text.includes('already exist') ||

View on GitHub (pinned to 5be4028344)

Solutions

  1. Retry the deploy after a short wait; transient edge errors usually clear.
  2. Check Vercel status for an active incident.
  3. Confirm the Vercel token is still valid.
  4. Remove any intercepting proxy for api.vercel.com.
Defensive patterns

Strategy: retry

Validate before calling

async function assertVercelJsonResponse(resp: Response): Promise<void> {
  const ct = resp.headers.get('content-type') ?? '';
  if (!ct.includes('application/json')) {
    throw new Error(`Expected JSON from Vercel, got ${ct || 'unknown'} (status ${resp.status}).`);
  }
}

await assertVercelJsonResponse(resp);

Type guard

function isVercelJsonError(err: unknown): boolean {
  return err instanceof DeployError && /non-JSON response/i.test(err.message);
}

Try / catch

async function vercelJsonWithRetry(build: () => Promise<Response>, attempts = 3): Promise<JsonObject> {
  for (let i = 1; i <= attempts; i++) {
    try {
      return await readVercelJson(await build());
    } catch (err) {
      if (err instanceof DeployError && /non-JSON response/i.test(err.message) && i < attempts) {
        await new Promise((r) => setTimeout(r, 1000 * i));
        continue;
      }
      throw err;
    }
  }
  throw new Error('unreachable');
}

Prevention

When it happens

Trigger: Vercel's API returns a non-JSON body: a 5xx HTML error, an auth/login interstitial, a Vercel challenge page, or an empty gateway timeout response.

Common situations: Transient Vercel outage; token revoked surfacing a login page; corporate proxy returning HTML for api.vercel.com; rate-limit edge returning plain text.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/1c7fe69af4c92df3. Report an issue: GitHub.