nexu-io/open-design · error · DeployError

Cloudflare returned a non-JSON response.

Error message

Cloudflare returned a non-JSON response.

What it means

readCloudflareJson calls resp.json(); if the response body is not valid JSON the parse throws and the daemon wraps it as a DeployError with the original HTTP status (or 502 if no status). This means Cloudflare returned an HTML error page, an empty body, or plain-text gateway text instead of the expected JSON envelope.

Source

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

function cloudflareHeaders(config: DeployConfig, extra: Record<string, string> = {}) {
  return {
    Authorization: `Bearer ${config.token}`,
    ...extra,
  };
}

function cloudflareAssetHeaders(token: string, extra: Record<string, string> = {}) {
  return {
    Authorization: `Bearer ${token}`,
    ...extra,
  };
}

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

async function fetchCloudflarePaginatedResult(config: DeployConfig, buildUrl: (page: number, perPage: number) => string, fallback: string, options: { perPage?: number } = {}) {
  const results: JsonObject[] = [];
  const perPage = options.perPage || CLOUDFLARE_API_PAGE_SIZE;
  for (let page = 1; page <= CLOUDFLARE_API_MAX_PAGES; page += 1) {
    const resp = await fetch(buildUrl(page, perPage), {
      headers: cloudflareHeaders(config),
    });
    const json = await readCloudflareJson(resp);
    if (!resp.ok || json?.success === false) {
      throw cloudflareError(json, resp.status, fallback);
    }
    const pageItems = Array.isArray(json?.result) ? json.result : [];
    results.push(...pageItems);
    if (!shouldFetchNextCloudflarePage(json?.result_info, page, perPage, pageItems.length)) break;
  }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Retry the request after a short wait; most non-JSON responses are transient edge errors.
  2. Check the Cloudflare status page for an active incident.
  3. Verify the API token is still valid and not revoked.
  4. Disable any intercepting proxy for api.cloudflare.com.
Defensive patterns

Strategy: retry

Validate before calling

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

await assertJsonResponse(resp);

Type guard

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

Try / catch

async function cloudflareJsonWithRetry(build: () => Promise<Response>, attempts = 3): Promise<JsonObject> {
  for (let i = 1; i <= attempts; i++) {
    try {
      return await readCloudflareJson(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: Cloudflare's edge returns a non-JSON body: a 5xx HTML error page, a Cloudflare WAF/challenge interstitial, a login/auth wall, or an empty body from a gateway timeout. Any of these makes resp.json() throw.

Common situations: Transient Cloudflare outage; corporate proxy intercepting api.cloudflare.com and returning HTML; token revoked so Cloudflare serves an auth page; DNS/CDN-level incident returning a static page.

Related errors


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