1 Commits

Author SHA1 Message Date
Deep Gori a1873e90a7 fix(core): make HTTP status errors catchable as ComposioError (#4543)
## Summary

In the Python SDK, HTTP failures from the generated client escape
`except ComposioError`: `composio_client` has its own exception root,
unrelated to `composio.exceptions.ComposioError`. An invalid API key, a
429 or a 500 therefore bypasses a handler written against the SDK's base
error, while the TypeScript SDK covers the equivalent case (#4459).

Opened this so there's something concrete to look at alongside the
issue. Happy to rework it or close it if you'd prefer a different
approach.

Fixes #4537

## Changes

- `HttpClient` overrides `_make_status_error`, the single place the
generated client builds status errors, and returns each error as a
subclass of **both** the generated class and `ComposioError` (one cached
subclass per generated class).
- New `python/tests/test_client_errors.py` covering every mapped status
plus an unmapped one, class reuse, and the SDK's existing
`ToolNotFoundError` mapping.

I went with this rather than wrapping errors at call sites, which is
what I suggested on the issue, because it keeps every existing handler
working:

- `except ComposioError` now catches HTTP failures.
- `except composio_client.AuthenticationError` / `APIStatusError` still
work, with `status_code`, `response` and `body` unchanged.
- The SDK's own mappings are untouched: `get_raw_composio_tool_by_slug`
still raises `ToolNotFoundError` on 400/404 and re-raises other client
errors unchanged, per its docstring and `test_tool_retrieval_errors.py`.
The same holds for `TriggerTypeNotFound`.
- No call sites change, so every endpoint is covered, including ones
added later.

On relying on a private method: `_make_status_error` is the hook the
generated base client declares (`raise NotImplementedError()`) and calls
for every status error, and `HttpClient` already overrides
`_prepare_request` from the same base. `composio-client` is pinned
exactly, and the new tests run through a real `HttpClient`, so a
generator change that altered the hook would fail CI at the version bump
rather than silently regress.

Left alone:

- **Transport-level errors.** `APIConnectionError` and `APITimeoutError`
are raised by the base client without going through
`_make_status_error`, so an HTTP timeout still escapes `except
ComposioError`. (The issue said timeouts were already covered; that was
true only for the SDK's own `ComposioSDKTimeoutError` from
`wait_for_connection`, not for HTTP timeouts.) Happy to follow up if you
want those covered too.
- **The existing `composio.client.ComposioAPIError` alias** still points
at the generated `APIError`, unchanged. The name I floated on the issue
would have collided with it, and this approach doesn't need a new public
class.

## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change

## How Has This Been Tested?

`python/tests/test_client_errors.py` runs a real `HttpClient` against an
`httpx.MockTransport`. Each status (400, 401, 403, 404, 409, 422, 429,
500, and an unmapped 418) raises an error that is both a `ComposioError`
and the expected generated class, with `status_code` preserved. Without
the fix, 12 of the 13 new tests fail.

From `python/`, Python 3.12, `composio-client==1.43.0`:

```
$ pytest tests/ -q
2072 passed, 1 skipped
$ ruff check --config config/ruff.toml composio tests
All checks passed!
$ ruff format --config config/ruff.toml --check composio/client/__init__.py tests/test_client_errors.py
2 files already formatted
$ mypy --config-file config/mypy.ini composio
Success: no issues found in 58 source files
```

Live check against production with an invalid key:

```python
from composio import Composio
from composio.exceptions import ComposioError

try:
    Composio(api_key="ak_invalid").create(user_id="u")
except ComposioError as e:
    print(type(e), e.status_code)
```

On `next` this raises `composio_client.AuthenticationError`, which
escapes the handler. On this branch the handler catches it, and it is
still an `AuthenticationError` with status 401.

## Checklist
- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [ ] I updated documentation as needed (no docs change; the public API
is unchanged)
- [x] I added tests or explain why not applicable
- [ ] I added a changeset if this change affects published packages
(Python-only change; CONTRIBUTING asks for changesets on published
TypeScript packages)

## Additional context

Found while integrating the Python SDK into
[Inferra](https://github.com/deepgori/inferra), where a GitHub-issue
filer caught `ComposioError` and missed the invalid-key path.

---------

Co-authored-by: jkomyno <alberto@composio.dev>
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-09-21 21:46:27 +04:00