ComposioHQ/composio · error · ErrorUploadingFile

Failed to upload to S3. Status: {response.status_code}. This

Error message

Failed to upload to S3. Status: {response.status_code}. This may indicate an expired presigned URL or permission issue.

What it means

While building the custom tools map from a response, two top-level custom tool handles share the same slug (compared case-insensitively via .upper()). Every custom tool must have a unique slug across all custom tools and toolkits.

Source

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

    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
            presigned URL was requested with, otherwise S3 rejects the PUT
            with ``403 SignatureDoesNotMatch`` when the signature covers the
            content type.

    Returns:

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Dedupe the tools list before passing it (by slug.upper())
  2. Rename one of the duplicate-slug tools

Example fix

# before
tools = [t1, t2]  # both slug='SEND'
# after
seen = set()
tools = [t for t in [t1, t2] if not (t.slug.upper() in seen or seen.add(t.slug.upper()))]
Defensive patterns

Strategy: validation

Validate before calling

keys = [h.slug.upper() for h in tools]
if len(keys) != len(set(keys)):
    raise ValueError(f'Duplicate tool slugs: {sorted({k for k in keys if keys.count(k) > 1})}')
result = build_custom_tools_map_from_response(tools, toolkits)

Type guard

def unique_tool_slugs(tools: list) -> bool:
    keys = [h.slug.upper() for h in tools]
    return len(keys) == len(set(keys))

Try / catch

try:
    m = build_custom_tools_map_from_response(tools, toolkits)
except ValidationError as e:
    if 'unique slug' in str(e):
        seen, deduped = set(), []
        for h in tools:
            k = h.slug.upper()
            if k not in seen:
                seen.add(k)
                deduped.append(h)
        m = build_custom_tools_map_from_response(deduped, toolkits)
    else:
        raise

Prevention

When it happens

Trigger: Passing a tools list to build_custom_tools_map_from_response (via create/use) containing two CustomTool handles whose slug.upper() matches.

Common situations: Concatenating tool lists that each define the same tool; copy-pasting tool definitions; case-variant slugs like 'Send_Email' and 'send_email'.

Related errors


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