Skip to content

fix(clients): keep stats() and Retry-After inside the error contract - #2116

Merged
eldad-caura-ai merged 1 commit into
mainfrom
fix/client-stats-retry-after
Oct 20, 2026
Merged

eldad-caura-ai merged 1 commit into
mainfrom
fix/client-stats-retry-after

Conversation

@eldad-caura-ai

@eldad-caura-ai eldad-caura-ai commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

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 fix(client-python): raise CauraAPIError for non-JSON 2xx bodies #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

  • Tests alone failed in CI (commit 944953f), all on the new cases:
    • Client (Python) CI, run 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: 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

@github-actions

Copy link
Copy Markdown
Contributor

Claude Code Review — skipped: draft PR

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
eldad-caura-ai force-pushed the fix/client-stats-retry-after branch from 944953f to 7462ee1 Compare October 20, 2026 10:00
@eldad-caura-ai
eldad-caura-ai marked this pull request as ready for review October 20, 2026 10:00
@eldad-caura-ai
eldad-caura-ai requested a review from a team as a code owner October 20, 2026 10:00
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Review by Claude Code

Claude Code Review ✅ No issues found.


Reviewed by claude-sonnet-5-5 · cost $0.12635775

@eldad-caura-ai

Copy link
Copy Markdown
Member Author

@erni-a pleas rparprove

@eldad-caura-ai

Copy link
Copy Markdown
Member Author

@erni-a please review

@eldad-caura-ai
eldad-caura-ai merged commit f796a6c into main Oct 20, 2026
29 checks passed
@eldad-caura-ai
eldad-caura-ai deleted the fix/client-stats-retry-after branch October 20, 2026 10:13
@caura-deploy-bot caura-deploy-bot Bot mentioned this pull request Oct 20, 2026
eldad-caura-ai pushed a commit that referenced this pull request Oct 20, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>backend: 3.34.6</summary>

##
[3.34.6](backend-v3.34.5...backend-v3.34.6)
(2026-10-20)


### Bug Fixes

* **clients:** keep stats() and Retry-After inside the error contract
([#2116](#2116))
([f796a6c](f796a6c))
* **collaboration:** install the join client from main, not a deleted
branch ([#2111](#2111))
([8d439c3](8d439c3))
* **field:** bound memory-map builds and see edits in its cache
([#2110](#2110))
([c222908](c222908))
</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 20, 2026
## Summary

Bumps both SDK clients to **1.1.1** so the next publish carries #2116,
the only client change since 1.1.0. The 1.1.0 tags are
`caura-client-v1.1.0` and `caura-client-ts-v1.1.0` at fd1da78.

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

## What 1.1.1 carries

#2116, in both clients:
- **L-237:** `stats()` rejects a 2xx body that is not JSON, or not a
JSON object, with `CauraAPIError` / `CauraApiError`, as `search()` and
`recall()` do. Before, Python leaked `JSONDecodeError` or
`AttributeError`, and TypeScript returned zero counters.
- **L-238:** `RateLimitError.retry_after` is `None`/`null` unless the
delay is finite and non-negative. Before, Python passed through `nan`,
`inf` and negatives, and TypeScript passed negatives and read a blank
header as 0.

**Why a patch:** fixes only, with no new API. A caller only sees a
difference for a malformed response or header.

## Release, after merge

Each tag runs its publish workflow, which checks that the tag's version
matches the package:
- `caura-client-v1.1.1` → `publish-python-client.yml` (PyPI
`caura-client`)
- `caura-client-ts-v1.1.1` → `publish-npm-client.yml` (npm
`@caura/client`)

The wrapper packages depend on `caura-client>=1.0.0` (`caura`,
`caura-sdk`), `caura-client>=1.0.3` (the memclaw shim) and
`@caura/client ^1.0.0`/`^1.0.1` (npm), so they pick up 1.1.1 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>
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.

2 participants