can1357/oh-my-pi · error · DestinationUnavailableError

the destination does not expose the local blob server

Error message

the destination does not expose the local blob server

What it means

During #start, after the local blob HTTP server binds, the backend re-validates that the configured destination kind actually exposes the local server (isServeKind). If not, it throws DestinationUnavailableError. In practice this is a defensive consistency check: non-serve kinds take the uploader path and never reach #start, so hitting it means the kind/config changed underneath the backend.

Source

Thrown at packages/coding-agent/src/blob-broker/broker.ts:142

	async #start(): Promise<string | null> {
		try {
			this.#server = Bun.serve({
				hostname: this.#config.bindHost,
				port: 0,
				fetch: request => {
					if (new URL(request.url).pathname === "/.well-known/omp-blob-health") {
						return new Response(null, { status: 204 });
					}
					return this.#store.serve(request);
				},
			});
			this.#server.unref();
			const port = this.#server.port;
			if (port === undefined) throw new Error("blob server bound without a TCP port");
			const kind = this.#config.kind;
			if (!isServeKind(kind)) {
				throw new DestinationUnavailableError(kind, "the destination does not expose the local blob server");
			}
			const exposureConfig: ExposureConfig = {
				kind,
				publicBaseUrl: this.#config.publicBaseUrl,
				bindHost: this.#config.bindHost,
				sshTarget: this.#config.sshTarget,
				sshRemotePort: this.#config.sshRemotePort,
				options: this.#config.options,
				credentials: this.#config.credentials,
			};
			const exposure = await startExposure(exposureConfig, port);
			try {
				await probeExposureHealth(exposure.baseUrl);
			} catch (error) {
				exposure.stop();
				throw error;
			}
			this.#exposure = exposure;

View on GitHub (pinned to 9690622007)

Solutions

  1. Use a valid serve kind (direct, ssh, or one of the tunnel kinds) for the backend config.
  2. Ensure the config passed to LocalBlobBackend is not mutated after construction; rebuild the backend with the new config instead.
  3. Route upload-only destinations through their uploader path so #start is never invoked.

Example fix

// before
const backend = new LocalBlobBackend(config); // kind: custom upload id, uploader missing
await backend.ensureStarted();
// after
const backend = new LocalBlobBackend({ ...config, kind: "direct" });
await backend.ensureStarted();
Defensive patterns

Strategy: try-catch

Validate before calling

import { isServeKind } from "./blob-broker/broker";
if (!isServeKind(config.kind)) {
  throw new Error(`kind ${config.kind} is not a serve destination; it must go through the uploader path`);
}

Try / catch

try {
  await backend.ensureStarted();
} catch (err) {
  if (err instanceof DestinationUnavailableError) {
    logger.warn("backend kind inconsistent with serve path", { kind: config.kind });
  } else throw err;
}

Prevention

When it happens

Trigger: ensureStarted() on a LocalBlobBackend whose config.kind is not a serve kind but which was constructed without an uploader — an internally inconsistent config, or kind mutated after construction.

Common situations: Custom/test code constructing a backend with an uploader-like kind but bypassing the constructor's uploader resolution; config object mutated between construction and start; exotic custom BlobDestinationId values.

Related errors


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