{"record":{"id":"f481d37bcdb930bc","repo":"can1357/oh-my-pi","slug":"destination-option-key-must-be-a-string","errorCode":null,"errorMessage":"Destination option ${key} must be a string","messagePattern":"Destination option (.+?) must be a string","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploader-runtime.ts","lineNumber":56,"sourceCode":"\tconstructor(destination: BlobDestinationId, reason: string) {\n\t\tsuper(`${destination} is unavailable: ${reason}`);\n\t\tthis.name = \"DestinationUnavailableError\";\n\t\tthis.destination = destination;\n\t}\n}\n\n/** Read a required option without coercing its configured type. */\nexport function requireOption(config: DestinationRuntimeConfig, key: string): DestinationOptionValue {\n\tconst value = config.options[key];\n\tif (value === undefined) throw new Error(`Missing required destination option: ${key}`);\n\treturn value;\n}\n\n/** Read a string option, returning a fallback when it is absent. */\nexport function optionString(config: DestinationRuntimeConfig, key: string, fallback?: string): string | undefined {\n\tconst value = config.options[key];\n\tif (value === undefined) return fallback;\n\tif (typeof value !== \"string\") throw new Error(`Destination option ${key} must be a string`);\n\treturn value;\n}\n\n/** Read a number option, returning a fallback when it is absent. */\nexport function optionNumber(config: DestinationRuntimeConfig, key: string, fallback?: number): number | undefined {\n\tconst value = config.options[key];\n\tif (value === undefined) return fallback;\n\tif (typeof value !== \"number\" || !Number.isFinite(value)) {\n\t\tthrow new Error(`Destination option ${key} must be a finite number`);\n\t}\n\treturn value;\n}\n\n/** Read a boolean option, returning a fallback when it is absent. */\nexport function optionBoolean(config: DestinationRuntimeConfig, key: string, fallback?: boolean): boolean | undefined {\n\tconst value = config.options[key];\n\tif (value === undefined) return fallback;\n\tif (typeof value !== \"boolean\") throw new Error(`Destination option ${key} must be a boolean`);","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploader-runtime.ts#L38-L74","documentation":"optionString reads a destination config option and throws when the value exists but is not a string. The blob-broker uploader runtime treats destination options as loosely typed user config (from JSON/TOML-like config files), so it validates each option's runtime type before use. Throwing a clear per-key message beats silently coercing a wrong-typed value like a number or nested object into a string.","triggerScenarios":"Calling optionString(config, key) — directly or via wrappers like server, configFile, tunnelName, ttl, uploadUrl, fileField — when config.options[key] is defined but typed as number, boolean, array, or object instead of string.","commonSituations":"Writing `port = 8080` (unquoted number) or `uploadUrl = true` in a destination config file; JSON config with a numeric value where a string URL is required; YAML/JSON5 parsers inferring types; copy-pasting config where quotes were lost.","solutions":["Quote the value in the destination config so it parses as a string (e.g. `\"port\": \"8080\"`).","Check the destination's expected option names/types in the uploader docs and fix the key's value type.","If the value is genuinely numeric (like a port), confirm the destination uploader supports optionNumber and move it to the numeric option key."],"exampleFix":"// before (config)\n{ \"options\": { \"uploadUrl\": 8080 } }\n// after\n{ \"options\": { \"uploadUrl\": \"https://example.com/upload\" } }","handlingStrategy":"validation","validationCode":"function assertStringOption(options: Record<string, unknown>, key: string): void {\n  const v = options[key];\n  if (v !== undefined && typeof v !== \"string\") throw new Error(`option ${key} must be a string, got ${typeof v}`);\n}\nassertStringOption(config.options, \"uploadUrl\");","typeGuard":"const isString = (v: unknown): v is string => typeof v === \"string\";","tryCatchPattern":"try {\n  const url = optionString(config, \"uploadUrl\");\n} catch (err) {\n  logger.warn(\"destination config invalid\", { option: \"uploadUrl\", err: String(err) });\n}","preventionTips":["Quote all string values in destination config files.","Run config through a schema validator (zod/arktype) at load time before handing options to the runtime.","Keep numeric values (ports, TTLs) in their dedicated numeric option keys, not string ones."],"tags":["config","validation","type-error"],"backgroundTag":"invalid-config-option-type","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}