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`.

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.

Appendix: 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)