windmill-labs/windmill · error · Error

Invalid s3 object URI ${JSON.stringify(s3Object)}: expected

Error message

Invalid s3 object URI ${JSON.stringify(s3Object)}: expected s3://<storage>/<key> with a non-empty key (s3:///<key> for the default storage)

What it means

parseS3Object accepts either an S3ObjectRecord object or an s3:// URI string. When given a string that starts with s3:// but does not match /^s3:\/\/([^/]*)\/(.+)$/ — i.e. an empty key like "s3://storage/" or "s3://" — it throws this error explaining the expected shape: s3://<storage>/<key>, with s3:///<key> meaning the default storage. It is distinct from the sibling error for strings that are not s3 URIs at all.

Source

Thrown at typescript-client/s3Types.ts:58

  /** Use path-style URLs instead of virtual-hosted style */
  pathStyle?: boolean;
};

/**
 * Parse an S3 object from URI string or record format
 * @param s3Object - S3 object as URI string (`s3://storage/key`, `s3:///key`
 *   for the default storage) or record. Any other string throws rather than
 *   falling back to an auto-generated key: an auto key is requested by
 *   omitting the object, and a fallback would silently misplace the upload
 *   on any typo.
 * @returns S3 object record with storage and s3 key
 */
export function parseS3Object(s3Object: S3Object): S3ObjectRecord {
  if (typeof s3Object === "object") return s3Object;
  const match = s3Object.match(/^s3:\/\/([^/]*)\/(.+)$/);
  if (match) return { storage: match[1] || undefined, s3: match[2] };
  if (s3Object.startsWith("s3://")) {
    throw new Error(
      `Invalid s3 object URI ${JSON.stringify(s3Object)}: expected s3://<storage>/<key> with a non-empty key (s3:///<key> for the default storage)`
    );
  }
  throw new Error(
    `Invalid s3 object ${JSON.stringify(s3Object)}: expected an s3://<storage>/<key> URI (e.g. "s3:///${s3Object}" for key "${s3Object}" in the default storage) or { s3: <key> }`
  );
}

View on GitHub (pinned to e474e8803c)

Solutions

  1. Ensure the key portion after the storage segment is non-empty before calling: assert against the URI regex or check for a trailing slash.
  2. For the default storage use s3:///<key> — an empty storage segment is allowed, an empty key is not.
  3. Fix the source of the empty key (undefined variable, empty DB field) rather than defaulting silently.
  4. For bare keys, pass the plain key string or { s3: key }; that produces the sibling 'Invalid s3 object' message instead, which is clearer for non-URI inputs.

Example fix

// before
const uri = `s3://${storage}/${fileKey}`; // fileKey undefined -> "s3://myStorage/"
await s3Obj(uri); // throws
// after
if (!fileKey) throw new Error('fileKey is required');
const uri = `s3://${storage}/${fileKey}`;
await s3Obj(uri); // "s3://myStorage/report.csv"
Defensive patterns

Strategy: validation

Validate before calling

function assertS3Uri(uri: string): void {
  if (uri.startsWith('s3://')) {
    const m = uri.match(/^s3:\/\/([^/]*)\/(.+)$/);
    if (!m) throw new Error(`bad s3 URI (empty key?): ${uri}`);
  }
}

Type guard

const isS3ObjectUri = (v: unknown): v is `s3://${string}` =>
  typeof v === 'string' && v.startsWith('s3://') && /^s3:\/\/([^/]*)\/.+$/.test(v);

Try / catch

try {
  await s3Obj(uri);
} catch (e) {
  if (e.message.startsWith('Invalid s3 object URI')) {
    await s3Obj({ s3: rawKey }); // fall back to object form / default storage
  } else throw e;
}

Prevention

When it happens

Trigger: Passing "s3://" or "s3://storage/" (empty key) to s3Obj, getPresignedS3PublicUrls, or any API resolving an S3Object; building the URI by concatenation where the key part is empty/undefined.

Common situations: Template interpolation `s3://${bucket}/${key}` with an undefined key; trailing-slash values from folder listings; copying a bucket root URI instead of an object key; upstream data returning empty key fields.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/aeb07f693c0ae526. Report an issue: GitHub.