ComposioHQ/composio · error · UnsafePathComponentError

Refusing to write {label} containing invalid Unicode: {name!

Error message

Refusing to write {label} containing invalid Unicode: {name!r}

What it means

safe_basename catches UnicodeEncodeError from os.fsencode(basename) — the filename contains characters that cannot be encoded with the filesystem encoding (e.g. surrogates from decoding bytes with errors='replace' on a UTF-8 fs) and is refused rather than crashing at write time.

Source

Thrown at python/composio/utils/safe_path.py:194

    if "\x00" in raw_basename:
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing a NUL byte: {name!r}"
        )
    if any(ord(char) < 32 or char in '<>:"|?*' for char in raw_basename):
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing characters reserved by "
            f"Windows: {name!r}"
        )
    if raw_basename.endswith((" ", ".")):
        raise UnsafePathComponentError(
            f"Refusing to write {label} ending in a space or dot: {name!r}"
        )

    basename = raw_basename.strip()
    try:
        encoded_length = len(os.fsencode(basename))
    except UnicodeEncodeError as e:
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing invalid Unicode: {name!r}"
        ) from e
    if encoded_length > MAX_COMPONENT_LENGTH:
        raise UnsafePathComponentError(
            f"Refusing to write {label} longer than {MAX_COMPONENT_LENGTH} bytes: "
            f"{basename[:32]!r}... ({encoded_length} bytes)"
        )
    # Compare everything before the first dot: on Windows `NUL.tar.gz` opens
    # the null device just as `NUL` does, so any number of extensions provides
    # no protection.
    device_name = basename.split(".", 1)[0].rstrip(" ").upper()
    if device_name in WINDOWS_RESERVED_NAMES:
        raise UnsafePathComponentError(
            f"Refusing to write {label} that is a reserved device name: {name!r}"
        )
    return basename

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Re-encode lossily: name.encode('utf-8', 'replace').decode('utf-8') to drop unencodable characters
  2. Ensure the process runs with a UTF-8 filesystem encoding (LANG/LC_ALL, PYTHONUTF8=1)
  3. Generate an ASCII fallback filename for unencodable input

Example fix

# before
secure_basename_join(base, name)
# after
name = name.encode('utf-8', 'replace').decode('utf-8')
secure_basename_join(base, name)
Defensive patterns

Strategy: fallback

Validate before calling

def encodable(v):
    try:
        os.fsencode(v)
        return True
    except UnicodeEncodeError:
        return False

Type guard

import os
def is_fs_encodable(v: str) -> bool:
    try:
        os.fsencode(v); return True
    except UnicodeEncodeError:
        return False

Try / catch

from composio.exceptions import UnsafePathComponentError
try:
    p = secure_basename_join(base, name)
except UnsafePathComponentError:
    p = secure_basename_join(base, name.encode('utf-8','replace').decode('utf-8'))

Prevention

When it happens

Trigger: secure_basename_join(base, name) where name contains lone surrogate code points (\udcXX) — typical when arbitrary bytes were decoded with surrogateescape and the locale/filesystem encoding can't represent them.

Common situations: See trigger scenarios.

Related errors


AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28). Data as JSON: /api/errors/d063afd8436c5d18. Report an issue: GitHub.