can1357/oh-my-pi · error · Error

Destination option protocol must be ftp, ftps, or sftp

Error message

Destination option protocol must be ftp, ftps, or sftp

What it means

Thrown by createFtpUploader when the ftp destination's options.protocol is set to a value other than "ftp", "ftps", or "sftp". The broker supports exactly three transfer protocols for the ftp destination id; anything else (including case variants like "FTP" or "SFTP") is rejected before any upload attempt.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts:178

function errorCode(error: unknown): unknown {
	if (typeof error !== "object" || error === null || !("code" in error)) return undefined;
	return error.code;
}

function ftpUploadUrl(protocol: "ftp" | "ftps", host: string, port: number, destinationPath: string): string {
	const implicitTls = protocol === "ftps" && port === 990;
	const scheme = implicitTls ? "ftps" : "ftp";
	const bracketedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
	const url = new URL(`${scheme}://${bracketedHost}`);
	url.port = String(port);
	url.pathname = `/${encodedPath(pathParts(destinationPath))}`;
	return url.toString();
}

function createFtpUploader(config: DestinationRuntimeConfig): BlobUploader {
	const protocol = optionString(config, "protocol", "sftp");
	if (protocol !== "ftp" && protocol !== "ftps" && protocol !== "sftp") {
		throw new Error("Destination option protocol must be ftp, ftps, or sftp");
	}
	const host = requiredStringOption(config, "host");
	const username = requireCredential(config, "username");
	const directory = optionString(config, "path");
	const publicBase = requiredStringOption(config, "publicBaseUrl");
	httpBase(publicBase, "publicBaseUrl");

	if (protocol === "sftp") {
		const port = optionNumber(config, "port", 22) ?? 22;
		const keyPath = credentialString(config, "privateKey");
		const password = credentialString(config, "password");
		if (password && !keyPath) {
			throw new DestinationUnavailableError(
				"ftp",
				"SFTP password injection is unsupported by the shared SSH transport; configure a private-key path or SSH agent",
			);
		}
		if (keyPath?.includes("-----BEGIN")) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Set options.protocol to exactly one of "ftp", "ftps", or "sftp" (lowercase).
  2. If you meant SCP over SSH, use "sftp" — the shared SSH transport (writeRemoteFile) is used for it.
  3. For explicit FTPS (AUTH TLS on port 21), use "ftps" with a port other than 990; the uploader sends curl --ssl-reqd in that case.
  4. Remove the protocol option entirely to get the default ("sftp") if that is what you want.

Example fix

// before
{ "destination": "ftp", "options": { "protocol": "SFTP", "host": "box.example.com", "publicBaseUrl": "https://f.example.com" } }
// after
{ "destination": "ftp", "options": { "protocol": "sftp", "host": "box.example.com", "publicBaseUrl": "https://f.example.com" } }
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = new Set(['ftp', 'ftps', 'sftp']);
const protocol = config.options?.protocol ?? 'sftp';
if (!ALLOWED.has(protocol)) throw new Error(`protocol must be one of ftp, ftps, sftp (got ${protocol})`);

Type guard

const isFtpProtocol = (v) => v === 'ftp' || v === 'ftps' || v === 'sftp';

Try / catch

try {
  const uploader = createSelfHostedUploader('ftp', config);
} catch (err) {
  if (err instanceof Error && err.message.includes('must be ftp, ftps, or sftp')) {
    // fix options.protocol before retrying
  } else throw err;
}

Prevention

When it happens

Trigger: Configuring a destination with destination "ftp" and options.protocol set to e.g. "scp", "SFTP", "sftps", "ftpes", or an empty string.

Common situations: Users conflating scp with sftp; typing the protocol in uppercase because the error message shows uppercased names; inventing ftpes (explicit FTPS) which is not a supported value here; leaving protocol set from a previous template.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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