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
- Default to a generated filename when the source field is missing (e.g. f'download-{uuid4().hex}')
- Validate the field exists before calling the download/save API
- 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
- Default missing filename fields to generated names before download
- Type-check deserialized filename fields
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
- Refusing to build a path from an unsafe {label}: {value!r}.
- Refusing to write {label} containing a NUL byte: {name!r}
- Unsafe path component: {e}
- Refusing to build a path from an empty or non-string {label}
- Refusing to build a path from a {label} containing path sepa
AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28).
Data as JSON: /api/errors/ee88d989c5e82365.
Report an issue: GitHub.