can1357/oh-my-pi · error · LegacyDestinationError

the upload endpoint returned an invalid JSON object

Error message

the upload endpoint returned an invalid JSON object

What it means

This LegacyDestinationError is thrown by jsonObject() when the response body parses as JSON but is not a non-empty plain object (arrays, null, strings, numbers, and empty objects are all rejected). The uploaders index into the parsed value with string keys, so they require an object with at least one property.

Source

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

		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")}`;
}

function optionalBasicHeaders(
	destination: BlobDestinationId,
	config: DestinationRuntimeConfig,

View on GitHub (pinned to 9690622007)

Solutions

  1. Adjust the endpoint to return a JSON object containing the upload result fields (url/direct_url)
  2. If the endpoint returns an array or scalar, wrap it server-side in an object
  3. Check the response body to confirm it is `{}`/array/null and compare with the schema the uploader expects
Defensive patterns

Strategy: type-guard

Type guard

function isNonEmptyJsonObject(value: unknown): value is Record<string, unknown> {
  return (
    typeof value === "object" && value !== null && !Array.isArray(value) &&
    Object.keys(value).length > 0
  );
}

Try / catch

try {
  const result = await uploader.upload(request);
} catch (err) {
  if (err instanceof Error && err.message.includes("invalid JSON object")) {
    // body parsed but was null/array/scalar/{} — fix server response shape
  } else throw err;
}

Prevention

When it happens

Trigger: The endpoint returns `null`, `[]`, `"ok"`, `42`, or `{}` — e.g. an upload API that acknowledges success with an empty JSON object or a bare array of URLs; thrown right after the invalid-JSON check in jsonObject(), consumed by the `data` helper.

Common situations: Replacement endpoints whose success response is `{}` or a JSON array; APIs returning JSON-RPC style arrays; servers echoing empty bodies with a JSON content-type.

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/8efd373190dcb54b. Report an issue: GitHub.