can1357/oh-my-pi · error · LegacyDestinationError

the upload endpoint returned invalid JSON

Error message

the upload endpoint returned invalid JSON

What it means

This LegacyDestinationError is thrown by jsonObject() when `response.json()` fails — i.e. the upload endpoint's HTTP response body is not valid JSON (or the body was already consumed). The library expects every legacy JSON-based uploader response to parse as JSON, and wraps the SyntaxError as `cause`.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders-legacy.ts:129

}

function firstString(record: Readonly<Record<string, unknown>>, keys: readonly string[]): string | undefined {
	for (const key of keys) {
		const value = record[key];
		if (typeof value === "string" && value.trim()) return value.trim();
	}
	return undefined;
}

async function jsonObject(
	destination: BlobDestinationId,
	response: Response,
): Promise<Readonly<Record<string, unknown>>> {
	let value: unknown;
	try {
		value = await response.json();
	} catch (error) {
		throw new LegacyDestinationError(destination, "the upload endpoint returned invalid JSON", error);
	}
	const record = objectValue(value);
	for (const _ in record) return record;
	throw new LegacyDestinationError(destination, "the upload endpoint returned an invalid JSON object");
}

function directJsonUrl(destination: BlobDestinationId, record: Readonly<Record<string, unknown>>, base: URL): string {
	const nested = objectValue(record.response);
	const raw =
		firstString(record, ["direct_url", "directUrl", "url", "URL"]) ??
		firstString(nested, ["direct_url", "directUrl", "url", "URL"]);
	if (!raw) throw new LegacyDestinationError(destination, "the upload response did not include a direct image URL");
	return httpUrl(destination, raw, base);
}

function basicAuthorization(username: string, password: string): string {
	return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Log the raw response body and status before parsing to see what the server actually sent
  2. Fix authentication so the endpoint stops returning HTML error/login pages
  3. Ensure the custom endpoint always replies with a JSON object (even on errors)
  4. Check for CDN/WAF challenge pages (Cloudflare) and allowlist the client
Defensive patterns

Strategy: try-catch

Type guard

function looksLikeJson(response: Response): boolean {
  const ct = response.headers.get("content-type") ?? "";
  return ct.includes("application/json");
}

Try / catch

try {
  const result = await uploader.upload(request);
} catch (err) {
  if (err instanceof Error && err.message.includes("returned invalid JSON")) {
    console.error("cause:", err.cause); // SyntaxError details + check status/body
  } else throw err;
}

Prevention

When it happens

Trigger: The endpoint returns HTML (error page, login page, Cloudflare challenge), an empty body, plain text, or truncated JSON; a 5xx/403 page is returned with text/html content-type; the response stream was interrupted mid-body; called from the `data` helper on JSON-based uploaders.

Common situations: Self-hosted replacement endpoint returning its framework's HTML 404/500 page; auth failure redirecting to an HTML login page; rate-limit/CDN block pages; proxy returning non-JSON errors.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/3e0c94d35f66650d. Report an issue: GitHub.