paperclipai/paperclip · error

must be a non-empty string.

Error message

${key} must be a non-empty string.

What it means

parseConfig's text() helper validates each string config key: a present value must be a non-empty, non-whitespace string containing no NUL bytes. This error is thrown when a config key like apiUrl, apiKey, shape, rootfs, or region is provided but is not a usable string — for example a number, an empty string, whitespace, or a string containing "\0".

Solutions

  1. Inspect the raw config object and print the offending key's value and type.
  2. Coerce known-numeric values with String(value) before calling parseConfig, or fix the config source to emit strings.
  3. Trim or re-enter the value if it is empty/whitespace-only; provide the actual credential/URL.
  4. Re-upload the secret if it contains NUL or control bytes — it was likely corrupted.

Example fix

// before
const config = parseConfig({ apiUrl: process.env.CREATEOS_API_URL }); // undefined/empty env -> error
// after: validate and coerce first
const apiUrl = (process.env.CREATEOS_API_URL ?? "").trim();
if (!apiUrl) throw new Error("CREATEOS_API_URL is required");
const config = parseConfig({ apiUrl });
Defensive patterns

Strategy: validation

Validate before calling

function prevalidateConfig(raw) {
  for (const key of ["apiUrl", "apiKey", "shape", "rootfs", "region"]) {
    const v = raw[key];
    if (v == null) continue;
    if (typeof v !== "string" || !v.trim() || v.includes("\0"))
      throw new Error(`${key} must be a non-empty string before calling parseConfig`);
  }
}

Type guard

function isNonEmptyString(v: unknown): v is string {
  return typeof v === "string" && v.trim().length > 0 && !v.includes("\0");
}

Try / catch

try {
  config = parseConfig(raw);
} catch (err) {
  if (err.message.includes("must be a non-empty string")) {
    const key = err.message.split(" ")[0];
    throw new Error(`Configuration field '${key}' is invalid; check the board plugin settings and secret source.`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Setting a config value to "" or " ", passing a numeric/boolean value where a string is expected (e.g. region: 1), or supplying a value containing a NUL byte (often from decoding binary or corrupted secret storage).

Common situations: Environment variables interpolated to empty strings, secrets managers returning undefined coerced to "", YAML/JSON configs with wrong types, or pasted credentials containing invisible control characters.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/a9bed712e47207f5. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/config.ts:16

export interface CreateosConfig {
  apiUrl: string;
  apiKey: string | null;
  shape: string;
  rootfs: string | null;
  region: string | null;
  timeoutMs: number;
  reuseLease: boolean;
}

export function parseConfig(raw: Record<string, unknown>): CreateosConfig {
  const text = (key: string): string | null => {
    const value = raw[key];
    if (value == null) return null;
    if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
      throw new Error(`${key} must be a non-empty string.`);
    }
    return value.trim();
  };
  const apiUrl = text("apiUrl");
  if (!apiUrl) throw new Error("CreateOS requires an API URL.");
  let url: URL;
  try { url = new URL(apiUrl); } catch { throw new Error("CreateOS API URL is invalid."); }
  // Configuration is board-owned, but never follow redirects with the API key.
  // Plain HTTP is useful for a loopback development server only.
  const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
  if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
      url.username || url.password || url.search || url.hash ||
      !["", "/", "/v1", "/v1/"].includes(url.pathname)) {
    throw new Error("CreateOS API URL must be an HTTPS origin (optionally ending in /v1); HTTP is allowed on loopback only.");
  }
  const shape = text("shape");
  if (!shape) throw new Error("CreateOS requires a shape from its shape catalog.");
  const timeoutMs = raw.timeoutMs ?? 300_000;

View on GitHub (pinned to 3f1d897a7c)