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
- Retry the deploy after a short wait; transient edge errors usually clear.
- Check Vercel status for an active incident.
- Confirm the Vercel token is still valid.
- 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
- Inspect response Content-Type before parsing when calling Vercel directly.
- Retry non-JSON Vercel responses with backoff; they are typically transient.
- Keep the Vercel token valid to avoid HTML auth pages.
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
- Cloudflare returned a non-JSON response.
- Vercel token is required.
- Cloudflare reported an unknown asset hash: ${hash}
- UPSTREAM_UNAVAILABLE
- aihubmix image (gemini) ${resp.status}: ${text.slice(0, 240)
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/1c7fe69af4c92df3.
Report an issue: GitHub.