ComposioHQ/composio · error · UnsafePathComponentError

Refusing to write a non-string {label}: {name!r}

Error message

Refusing to write a non-string {label}: {name!r}

What it means

safe_basename refuses to derive a writable filename from a non-string value (None, int, bytes). The filename being sanitized must be a str before basename extraction and validation can proceed.

Source

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

    forbids ``.``, which nearly every real filename contains. This applies the
    remaining checks — no separators, no traversal, no NUL, bounded length, no
    reserved device name — to the one component a server most directly controls.

    ``PureWindowsPath`` treats both ``/`` and ``\\`` as separators, so a name
    crafted for a Windows target (``..\\..\\evil``) is stripped even when the
    SDK runs on POSIX, where ``Path(...).name`` would return it intact.

    Names that leave no usable basename are refused rather than replaced with a
    generated one: a response that cannot name its own file is malformed or
    hostile, and inventing a name would hide that. ``.`` and the empty string
    both basename to ``""``, which makes an output path equal to its own
    directory and surfaces as ``IsADirectoryError`` at write time.

    :raises UnsafePathComponentError: when ``name`` yields no usable basename or
        is unsafe to write.
    """
    if not isinstance(name, str):
        raise UnsafePathComponentError(
            f"Refusing to write a non-string {label}: {name!r}"
        )

    raw_basename = PureWindowsPath(name).name
    if not raw_basename or not raw_basename.strip() or set(raw_basename) == {"."}:
        raise UnsafePathComponentError(
            f"Path traversal detected: {label} {name!r} leaves no usable "
            "basename to write to."
        )
    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}"
        )

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Default to a generated filename when the source field is missing (e.g. f'download-{uuid4().hex}')
  2. Validate the field exists before calling the download/save API
  3. Coerce with str() only when the value is genuinely the filename

Example fix

# before
secure_basename_join(base, resp.filename)  # None
# after
import uuid
name = resp.filename or f"download-{uuid.uuid4().hex}"
secure_basename_join(base, name)
Defensive patterns

Strategy: type-guard

Validate before calling

def has_filename(v):
    return isinstance(v, str) and bool(v.strip())

Type guard

def is_string_filename(v) -> bool:
    return isinstance(v, str)

Try / catch

from composio.exceptions import UnsafePathComponentError
from uuid import uuid4
try:
    p = secure_basename_join(base, name)
except UnsafePathComponentError:
    p = secure_basename_join(base, f'download-{uuid4().hex}')

Prevention

When it happens

Trigger: secure_basename_join(base, name) where name is None (missing Content-Disposition/filename field) or a non-string type from deserialized JSON.

Common situations: Download responses missing a filename header; API fields absent rather than empty; strongly-typed IDs passed unconverted.

Related errors


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