can1357/oh-my-pi · error · Error

Destination option ${key} must be a string

Error message

Destination option ${key} must be a string

What it means

requiredString validates that a destination runtime option exists and is a string, throwing a plain Error when the value is present but of the wrong type (e.g. number or boolean). It guards options like accountId, bucket, container, and authorizationToken before they reach the S3/B2/object-storage uploaders.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders-object-storage.ts:71

	authorizationToken: string;
	apiUrl: string;
	downloadUrl: string;
}

interface B2Bucket {
	bucketId: string;
	bucketName: string;
	bucketType: string;
}

interface B2UploadTarget {
	uploadUrl: string;
	authorizationToken: string;
}

function requiredString(config: DestinationRuntimeConfig, key: string): string {
	const value = requireOption(config, key);
	if (typeof value !== "string") throw new Error(`Destination option ${key} must be a string`);
	return value;
}

function configuredPrefix(config: DestinationRuntimeConfig): string | undefined {
	return optionString(config, "keyPrefix") ?? optionString(config, "prefix") ?? optionString(config, "path");
}

function objectKey(prefix: string | undefined, filename: string): string {
	const cleanPrefix = prefix?.replaceAll("\\", "/").replace(/^\/+|\/+$/g, "");
	return cleanPrefix ? `${cleanPrefix}/${filename}` : filename;
}

function encodePath(path: string): string {
	return path
		.split("/")
		.map(segment =>
			encodeURIComponent(segment).replace(
				/[!'()*]/g,

View on GitHub (pinned to 9690622007)

Solutions

  1. Quote the option value in the destination config so it is a JSON string: "bucket": "12345".
  2. Inspect the config with a JSON validator to find which option under Destination option <key> is non-string.
  3. If the option is genuinely numeric upstream, stringify it in the config layer before handing it to the broker.
  4. Check for config-generation code that passes raw numbers instead of String(id).

Example fix

// before
{ "destination": { "accountId": 123456, "bucket": 98765 } }
// after
{ "destination": { "accountId": "123456", "bucket": "98765" } }
Defensive patterns

Strategy: validation

Validate before calling

const OPTION_KEYS = ["accountId", "bucket", "container", "uploadUrl", "authorizationToken"] as const;
for (const key of OPTION_KEYS) {
  const v = config[key];
  if (v !== undefined && typeof v !== "string") {
    throw new Error(`config option ${key} must be a string, got ${typeof v}`);
  }
}

Type guard

function isStringOption(config: Record<string, unknown>, key: string): config is Record<string, string> {
  return config[key] === undefined || typeof config[key] === "string";
}

Try / catch

try {
  const uploader = createObjectStorageUploader(destination, config);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Destination option")) {
    const key = err.message.match(/Destination option (\w+)/)?.[1];
    console.error(`Fix config: option '${key}' must be quoted as a string`);
  } else throw err;
}

Prevention

When it happens

Trigger: A destination config supplies an option that requiredString reads (bucket, accountId, container, uploadUrl, authorizationToken, etc.) with a non-string JSON value — e.g. "bucket": 12345 or "uploadUrl": true — or a number-like ID entered without quotes.

Common situations: Config file edited by hand where a numeric account/bucket ID was written as a JSON number; programmatic config builder passing numbers; YAML/JSON type coercion turning an all-digit ID into a number.

Related errors


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