can1357/oh-my-pi · error · DestinationUnavailableError

a user-supplied replacement endpoint is required

Error message

a user-supplied replacement endpoint is required

What it means

configuredEndpoint() throws a DestinationUnavailableError when a legacy destination has no user-supplied 'endpoint' option. Legacy image hosts whose original APIs died can only be used if you point them at a replacement (e.g. a self-hosted clone), so the endpoint is mandatory.

Source

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

}

interface SendSpaceNode {
	readonly url: URL;
	readonly maxFileSize: string;
	readonly uploadIdentifier: string;
	readonly extraInfo: string;
}

function failure(destination: BlobDestinationId, error: unknown): Error {
	if (error instanceof DestinationUnavailableError || error instanceof LegacyDestinationError) return error;
	const message = error instanceof Error ? error.message : String(error);
	return new LegacyDestinationError(destination, message, error);
}

function configuredEndpoint(destination: BlobDestinationId, config: DestinationRuntimeConfig): URL {
	const raw = optionString(config, "endpoint")?.trim();
	if (!raw) {
		throw new DestinationUnavailableError(destination, "a user-supplied replacement endpoint is required");
	}
	let endpoint: URL;
	try {
		endpoint = new URL(raw);
	} catch (error) {
		throw new LegacyDestinationError(destination, "the configured endpoint is not a valid URL", error);
	}
	if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:") {
		throw new LegacyDestinationError(destination, "the configured endpoint must use HTTP or HTTPS");
	}
	const hostname = endpoint.hostname.toLowerCase();
	const blocked = BLOCKED_CUSTOM_HOSTS[destination];
	if (blocked) {
		for (const domain of blocked) {
			if (hostname === domain || hostname.endsWith(`.${domain}`)) {
				throw new DestinationUnavailableError(destination, "the defunct public endpoint cannot be used");
			}
		}

View on GitHub (pinned to 9690622007)

Solutions

  1. Add an endpoint option with the replacement host URL, e.g. { "endpoint": "https://my-legacy-clone.example" }
  2. Check the option key spelling and that the value is non-empty after trimming
  3. Pick a non-legacy destination type if you have no replacement endpoint to supply

Example fix

// before
{ "kind": "legacy-host", "options": {} }
// after
{ "kind": "legacy-host", "options": { "endpoint": "https://replacement.example.com" } }
Defensive patterns

Strategy: validation

Validate before calling

const ep = config.options?.endpoint?.trim();
if (!ep) throw new Error(`legacy destination '${destination}' requires a user-supplied endpoint option`);
new URL(ep); // throws if malformed

Type guard

function hasLegacyEndpoint(config: DestinationRuntimeConfig): config is DestinationRuntimeConfig & { options: { endpoint: string } } {
	const ep = config.options?.endpoint;
	return typeof ep === "string" && ep.trim().length > 0;
}

Try / catch

import { DestinationUnavailableError } from "./errors";
try {
	const endpoint = configuredEndpoint(destination, config);
} catch (err) {
	if (err instanceof DestinationUnavailableError) {
		// prompt user to supply a replacement endpoint
	}
	throw err;
}

Prevention

When it happens

Trigger: Creating a legacy uploader (uploaders-legacy.ts) from a config that omits or has a blank 'endpoint' option — the trim() leaves an empty string and the guard fires.

Common situations: Configuring a legacy host by name only, assuming built-in defaults exist, or the endpoint option key being misspelled/empty after config parsing.

Related errors


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