Repository navigation
fix(client-typescript): raise CauraApiError for a non-JSON 2xx body - #2014
Merged
Merged
Conversation
readResponseJson rethrows SyntaxError on purpose, so raiseForStatus can tell that an error body isn't JSON. On the success path nothing caught it, so a 2xx with an HTML body (a captive portal, a misconfigured proxy) escaped request() as a bare SyntaxError, past callers catching CauraError. request() now turns it into CauraApiError(status, "response was not valid JSON") and keeps the SyntaxError as its cause; the error path is unchanged. CauraApiError takes an optional ErrorOptions so the cause is kept. Closes caura-ai#1901 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Geovana Grigorio <grigoricontato@gmail.com>
erni-a
approved these changes
Oct 11, 2026
erni-a
left a comment
Contributor
There was a problem hiding this comment.
Thanks @geoggrigori. Mirrors #2013 on the TypeScript side, keeps the SyntaxError as cause through the new ErrorOptions parameter, and the 502 test pins that non-JSON error bodies still map by status. Merging in this pass.
Merged
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>
1 task
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>
1 of 2 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In the TypeScript client, a 2xx response whose body isn't JSON (an HTML page from a captive portal or a misconfigured proxy) now rejects with
CauraApiError(200, "response was not valid JSON")instead of a bareSyntaxError. Callers that catchCauraErrorsee it.Related Issue
Closes #1901. This is the TypeScript side of #1899.
Type of Change
What changed
request()wraps the success-pathreadResponseJson(res)as the issue proposes. ASyntaxErrorbecomesCauraApiError(res.status, "response was not valid JSON"), and the originalSyntaxErroris kept aserror.cause. Any other error is rethrown as before, so a body-read failure is still aTransportError.readResponseJsonandraiseForStatusare untouched, so a non-JSON error body still maps by status.CauraApiErrortakes an optional fourthErrorOptionsargument, asTransportErroralready passes{ cause }. The existing three-argument calls and the subclasses are unchanged.How Has This Been Tested?
npm testinclients/typescript/(tsc +node --test): 56 passed.transport mapping does not wrap invalid JSONasserted the bareSyntaxErrorthe issue describes as the bug. It's replaced bya 2xx response that is not JSON is a CauraApiError, not a bare SyntaxError. That test covershealth()andwrite(), and checksinstanceof CauraApiErrorandCauraError, notTransportError,statusCode === 200, the message and theSyntaxErrorcause. It failed before the fix.a non-JSON error body still maps by status: a 502 with an HTML body is stillCauraApiError("[502] request failed"). It passes before and after, and guards the error path thatreadResponseJsonkeeps rethrowing for.prettier --checkpasses on both changed files.Checklist
ruff checkandruff format --checkpass: N/A, no Python touchedmypypasses: N/A, no Python touchedpytestpasses locally: N/A, no Python touched. The TypeScript suite passes (above).CHANGELOG.mdunder theUnreleasedsection (if user-facing): release-please builds it from thefix(client-typescript):subjectAdditional Notes
Commit is DCO signed-off.