Skip to content

fix(client-python): raise CauraAPIError for non-JSON 2xx bodies - #2013

Merged
erni-a merged 2 commits into
caura-ai:mainfrom
Girisha1908:fix/python-client-non-json-response
Oct 11, 2026
Merged

erni-a merged 2 commits into
caura-ai:mainfrom
Girisha1908:fix/python-client-non-json-response

Conversation

@Girisha1908

Copy link
Copy Markdown
Contributor

Summary

When a 2xx response has a body that isn't JSON (a captive portal page, a misconfigured proxy), every method on the Python client raised a bare json.JSONDecodeError. That isn't a CauraError, so except CauraError: didn't catch it. This PR raises CauraAPIError(status, "response was not valid JSON") instead, with the original error kept as __cause__.

Related Issue

Closes #1899

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Documentation update
  • Refactor / internal cleanup
  • Other:

How Has This Been Tested?

Added test_non_json_success_body_raises_api_error to clients/python/tests/test_client.py. It returns a 200 with an HTML body and runs against health, write, search, recall, get_document and submit_interview. All six cases fail on main and pass with this change.

Ran the same checks as client-python-ci.yml, on Linux:

cd clients/python
ruff check src tests   # All checks passed
pytest -q              # 209 passed

Checklist

  • I have read CONTRIBUTING.md
  • I have added tests that cover my changes (or explained why none are needed)
  • ruff check and ruff format --check pass
  • mypy passes
  • pytest passes locally
  • I have updated relevant documentation (README, docs, etc.)
  • I have updated CHANGELOG.md under the Unreleased section (if user-facing)

Additional Notes

test_transport_mapping_does_not_wrap_invalid_json in test_transport_errors.py expected the bare ValueError this PR removes. It now expects CauraAPIError.

Signed-off-by: Girisha <anamalagirisha@gmail.com>
@Girisha1908
Girisha1908 requested a review from a team as a code owner October 8, 2026 19:40

@erni-a erni-a left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Girisha1908. One helper, every entry point routed through it, the cause preserved, the status kept on the error, and the transport-error test updated to say what is now true. Merging in this pass; it shares test_client.py with #1991 and #2010 so expect me to update the branch before merge.

@erni-a
erni-a merged commit d6580f7 into caura-ai:main Oct 11, 2026
21 checks passed
@caura-deploy-bot caura-deploy-bot Bot mentioned this pull request Oct 11, 2026
eldad-caura-ai pushed a commit that referenced this pull request Oct 11, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>backend: 3.30.7</summary>

##
[3.30.7](backend-v3.30.6...backend-v3.30.7)
(2026-10-11)


### Bug Fixes

* a bulk item's null weight gets the column default (L-97)
([#2036](#2036))
([25bc89e](25bc89e))
* **client-python:** raise CauraAPIError for non-JSON 2xx bodies
([#2013](#2013))
([d6580f7](d6580f7))
* **client-typescript:** raise CauraApiError for a non-JSON 2xx body
([#2014](#2014))
([9ba140d](9ba140d))
* **clients:** parse HTTP-date Retry-After values
([#2010](#2010))
([f575803](f575803))
* **events:** type pull_executor concretely in _hold_leases
([#1596](#1596))
([#1698](#1698))
([45dff80](45dff80))
* **release:** keep the core-operations version in sync
([#1717](#1717))
([96a7907](96a7907))
* storage answers for the database it has (B28)
([#2033](#2033))
([67a0a57](67a0a57))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Signed-off-by: release-please[bot] <release-please[bot]@users.noreply.github.com>
Co-authored-by: caura-deploy-bot[bot] <265395343+caura-deploy-bot[bot]@users.noreply.github.com>
eldad-caura-ai added a commit that referenced this pull request Oct 15, 2026
## Summary

Bumps both SDK clients to **1.1.0** so the next publish carries what has
merged since the last releases (O14). Neither client has been published
since `caura-client` 1.0.3 on PyPI (tag `caura-client-v1.0.3`) and
`@caura/client` 1.0.2 on npm (tag `caura-client-ts-v1.0.2`).

Versions only: `clients/python/pyproject.toml` and
`caura_client/_version.py`; `clients/typescript/package.json`,
`package-lock.json` (its own two entries) and `src/version.ts`.

## What 1.1.0 carries

Both clients:
- #1807: refuse to send the API key over plain HTTP to another host
- #2010: parse HTTP-date `Retry-After` values
- #2012: `search()` keeps the response envelope (`SearchResult`, still a
list or array), `recall()` sends `items_alias: false`, and
`caller_agent_id` / `callerAgentId` lets a tenant-key client read as an
agent (opt-in)
- #2035: `stats()`
- #2071: the memory map, spend and value methods

Python: #2013 (`CauraAPIError` for a non-JSON 2xx body), plus #1775 and
#1909's client parts. TypeScript: #2014 (`CauraApiError` for a non-JSON
2xx body), #1667 (typing and response timeouts).

**Why minor:** new methods and fields. `search()` still returns a list
subclass (an array in TypeScript), so existing callers keep working. One
visible change: `recall()`'s `raw` no longer contains `items` unless
`items_alias` is passed.

**Server versions:** `stats()` needs a backend with #2035 (3.32.0 or
later) and the memory map, spend and value methods need #2071 (3.33.0 or
later). Against an older server those calls get a 404; everything else
works as before.

## Release, after merge

Each tag runs its publish workflow:
- `caura-client-v1.1.0` → `publish-python-client.yml` (PyPI
`caura-client`)
- `caura-client-ts-v1.1.0` → `publish-npm-client.yml` (npm
`@caura/client`)

The `caura`, `caura-sdk` (PyPI) and unscoped `caura-client` (npm)
packages depend on `caura-client>=1.0.0` / `@caura/client ^1.0.1`, so
they pick up 1.1.0 without a release of their own.

## Test plan

- [ ] CI: the Python client's version test (`__version__` agrees with
`pyproject.toml`) and the TypeScript client's build and tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: eldad-caura <eldad@caura.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
eldad-caura-ai added a commit that referenced this pull request Oct 20, 2026
L-237: stats(), added after #2013 and #2014, read its body without
the checks the other methods have. In Python a 2xx page that is not
JSON (a captive portal, a proxy) escaped as a bare JSONDecodeError,
and a JSON body that is not an object as AttributeError, though the
README says catching CauraError covers HTTP failures. In TypeScript
a body that is not an object came back as zero counters. Both now
raise "stats response must be a JSON object", as search() and
recall() do.

L-238: retry_after could hold a delay nobody can wait on. Python's
float() takes "nan", "inf", "-5" and digit strings past float range;
time.sleep() raises on NaN and negatives and overflows on infinity,
and asyncio.sleep(inf) never returns. TypeScript's Number() read
"-5" as -5 and a blank header as 0. Both now report None/null unless
the delay is finite and non-negative, as the collaboration client's
own parser already drops negatives.

Signed-off-by: eldad-caura <eldad@caura.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eldad-caura-ai added a commit that referenced this pull request Oct 20, 2026
…2116)

## Summary

Two Low findings in the client SDKs, from the security audit's review of
the client changes merged since its last recheck. Both are in the
clients' response handling, so they share one PR.

**L-237: `stats()` skips the response checks every other method has.**
It was added in #2035, after #2013 and #2014 made a non-JSON 2xx body a
`CauraAPIError`.
- **Python:** `stats()` reads `response.json()` directly. A
captive-portal or proxy page with a 200 status escapes as a bare
`JSONDecodeError`, although the README says catching `CauraError` covers
HTTP failures. A JSON body that isn't an object raises `AttributeError`.
- **TypeScript:** `stats()` returns zero counters with a non-object
`raw` for a body that isn't an object.

`search()` and `recall()` in both clients reject such bodies with `"...
response must be a JSON object"`.

**L-238: `retry_after` can hold a value nobody can wait on.**
- **Python:** `float()` accepts `nan`, `inf`, `-5`, and digit strings
past float range (which become `inf`). A caller that sleeps on it fails:
`time.sleep()` raises for NaN and negatives and overflows on infinity,
and `asyncio.sleep(inf)` never returns.
- **TypeScript:** `Number()` reads `-5` as -5 and a blank header as 0.

The collaboration client's own parser
(`caura_bus_core.retry.retry_after_seconds`) already rejects negatives.

## Fix

- **Both clients:** `stats()` rejects a body that is not a JSON object
with `"stats response must be a JSON object"`, the same check `search()`
and `recall()` make.
- **Python:** `stats()` reads its body through `_json()`, the helper
from #2013, so a 2xx page that is not JSON raises `CauraAPIError` like
every other call.
- **Retry-After:** `retry_after` is kept only when it is finite and
non-negative; otherwise it is `None`/`null`. TypeScript also treats a
blank header as absent. Valid delays and HTTP-dates (past dates still
give 0) are unchanged.

## Test plan

- [x] **Tests alone failed in CI** (commit 944953f), all on the new
cases:
- Client (Python) CI, run
[39404592562](https://github.com/caura-ai/caura/actions/runs/39404592562):
8 failed, 233 passed. The failures are `stats` in the non-JSON 2xx test
(`JSONDecodeError`), the three non-object `stats` bodies
(`AttributeError`), and `retry_after` for `-5`, `nan`, `inf` and a
400-digit value (`-5.0`, `nan`, `inf`, `inf`).
- Client (TypeScript) CI, run
[39404592529](https://github.com/caura-ai/caura/actions/runs/39404592529):
5 of 78 failed. They are the three non-object `stats` bodies (resolved
instead of rejecting), and `-5` and a blank header (`-5` and `0` instead
of `null`).
- [ ] Both pass with the fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: eldad-caura <eldad@caura.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(client-python): a 2xx response with a non-JSON body escapes as a bare JSONDecodeError, not a CauraError

2 participants