ComposioHQ/composio · error · ErrorUploadingFile

Failed to upload to S3: {_sanitize_url_for_logging(url)}. Er

Error message

Failed to upload to S3: {_sanitize_url_for_logging(url)}. Error: {type(e).__name__}

What it means

One original slug maps to multiple different final slugs, which would make tool resolution ambiguous. This happens when the same custom tool slug appears in contexts that produce different final slugs (e.g. the same bare slug both inside and outside a prefixed toolkit).

Source

Thrown at python/composio/core/models/_files.py:223

    Routing every presigned PUT through one helper keeps the file and bytes
    upload paths from drifting apart again, mirroring ``uploadFileToS3`` in
    the TypeScript SDK, which funnels path, URL, and File inputs through a
    single uploader.

    Raises:
        ErrorUploadingFile: On transport failure or a non-200 response,
            including the HTTP status when one was received.
    """
    try:
        response = safe_request(
            "PUT",
            url,
            data=data,
            headers={"Content-Type": mimetype},
            timeout=(_CONNECT_TIMEOUT, _READ_TIMEOUT),
        )
    except requests.exceptions.RequestException as e:
        raise ErrorUploadingFile(
            "Failed to upload to S3: "
            f"{_sanitize_url_for_logging(url)}. Error: {type(e).__name__}"
        ) from e
    if response.status_code != 200:
        raise ErrorUploadingFile(
            f"Failed to upload to S3. Status: {response.status_code}. "
            "This may indicate an expired presigned URL or permission issue."
        )


def upload(url: str, file: Path, mimetype: t.Optional[str] = None) -> bool:
    """Upload file to presigned S3 URL.

    Args:
        url: Presigned S3 upload URL
        file: Path to file to upload
        mimetype: Content type to send with the upload. Defaults to the type
            guessed from ``file``. This must match the ``mimetype`` the

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Remove the standalone copy of the tool that is also inside the toolkit
  2. Rename one instance so each original slug appears in only one context
  3. Audit how toolkits are combined before calling create/use

Example fix

# before
tools = [CustomTool(slug='FETCH', ...)]
toolkit = Toolkit(slug='gh', tools=[CustomTool(slug='FETCH', ...)])
# after
tools = []
toolkit = Toolkit(slug='gh', tools=[CustomTool(slug='FETCH', ...)])
Defensive patterns

Strategy: validation

Validate before calling

standalone = {h.slug.upper() for h in tools}
in_toolkits = {h.slug.upper() for tk in toolkits or [] for h in tk.tools}
overlap = standalone & in_toolkits
if overlap:
    raise ValueError(f'Slugs registered both standalone and in toolkits: {overlap}')

Type guard

def each_slug_single_context(tools: list, toolkits: list) -> bool:
    standalone = {h.slug.upper() for h in tools}
    in_tk = {h.slug.upper() for tk in toolkits for h in tk.tools}
    return not (standalone & in_tk)

Try / catch

try:
    tools_map = build_custom_tools_map(tools, toolkits)
except ValidationError as e:
    if 'maps to multiple final slugs' in str(e):
        tools = [h for h in tools if h.slug.upper() not in toolkit_slugs(toolkits)]
        tools_map = build_custom_tools_map(tools, toolkits)
    else:
        raise

Prevention

When it happens

Trigger: Registering a custom tool with slug X standalone and the same slug X inside a toolkit whose prefix produces a different final slug, so by_original_slug[X] already has an entry with a different final_slug, during build_custom_tools_map(_from_response).

Common situations: Mixing standalone tools and toolkit tools that share slugs; refactoring tools into toolkits while leaving standalone copies registered; dynamically composed tool lists.

Related errors


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