ComposioHQ/composio · error · UnsafePathComponentError

Refusing to write {label} longer than {MAX_COMPONENT_LENGTH}

Error message

Refusing to write {label} longer than {MAX_COMPONENT_LENGTH} bytes: {basename[:32]!r}... ({encoded_length} bytes)

What it means

safe_basename rejects filenames whose filesystem-encoded length exceeds MAX_COMPONENT_LENGTH (128) bytes. os.fsencode measures real bytes, so multi-byte UTF-8 names hit the cap sooner than their character count suggests, keeping writes safely under common 255-byte filename limits.

Source

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

    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


def resolve_root(root: t.Union[str, Path]) -> Path:
    """Normalize a trusted root to an absolute, symlink-resolved path.

    Every containment check must derive its anchor through this one function.

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Truncate the filename to a byte budget preserving the extension
  2. Replace long names with a short hash and keep the original in metadata

Example fix

# before
secure_basename_join(base, long_name)
# after
stem, _, ext = long_name.rpartition('.')
budget = 120 - len(ext.encode()) - 1
long_name = stem.encode('utf-8')[:budget].decode('utf-8', 'ignore') + '.' + ext
secure_basename_join(base, long_name)
Defensive patterns

Strategy: validation

Validate before calling

import os
MAX = 128
def within_byte_budget(v):
    try: return len(os.fsencode(v)) <= MAX
    except UnicodeEncodeError: return False

Type guard

import os
def is_short_filename(v: str) -> bool:
    try:
        return len(os.fsencode(v)) <= 128
    except UnicodeEncodeError:
        return False

Try / catch

from composio.exceptions import UnsafePathComponentError
try:
    p = secure_basename_join(base, name)
except UnsafePathComponentError:
    stem, _, ext = name.rpartition('.')
    p = secure_basename_join(base, stem[:64] + ('.' + ext if ext else ''))

Prevention

When it happens

Trigger: secure_basename_join(base, name) where len(os.fsencode(name)) > 128 — long descriptive filenames, CJK filenames (3 bytes/char), or hash-based names.

Common situations: Generated filenames embedding full titles or URLs; Unicode-heavy locales inflating byte length; download names composed from several API fields.

Related errors


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