Skip to content

feat: [CHA-2958] error hierarchy + wait_for_task#261

Open
mogita wants to merge 3 commits into
mainfrom
feat/cha-2958-error-handling
Open

feat: [CHA-2958] error hierarchy + wait_for_task#261
mogita wants to merge 3 commits into
mainfrom
feat/cha-2958-error-handling

Conversation

@mogita
Copy link
Copy Markdown
Contributor

@mogita mogita commented May 28, 2026

Summary

  • New error classes (importable from getstream or getstream.exceptions):
    • StreamException (abstract base)
    • StreamApiException — HTTP 4xx/5xx, full APIError envelope including unrecoverable and exception_fields
    • StreamRateLimitException — HTTP 429, adds retry_after: timedelta | None
    • StreamTransportException — network failures, error_type enum, __cause__ preserved
    • StreamTaskException — for wait_for_task failures
  • Stream.wait_for_task(...) (sync) and AsyncStream.wait_for_task(...) (async). 1s poll, 60s timeout by default.
  • Transport-layer httpx.RequestError is wrapped at the SDK boundary in both sync and async paths.
  • Retry-After parsing covers integer seconds and HTTP-date; past dates clamp to zero; missing / garbage → None.

Back-compat

getstream.base.StreamAPIException (capital API) preserved as an alias for one minor cycle via module-level __getattr__ that emits DeprecationWarning. Same class object as the new spelling, so pytest.raises(StreamAPIException), except StreamAPIException, and isinstance(...) keep working.

Test plan

  • uv run pytest tests/test_exceptions.py
  • uv run pytest (full suite)
  • uv run ruff check .

Summary by CodeRabbit

  • New Features

    • Standardized exception hierarchy exported at package level (API, rate-limit, transport, task)
    • Task polling helpers (sync & async) that return completed results, raise task errors on failure, and signal timeout as transport timeout
    • Webhook helper primitives and client-backed webhook methods
    • Connection pooling/timeouts with effective config logged
  • Changed

    • Network/request errors are wrapped and classified as transport exceptions; Retry-After is parsed for rate limits; default request timeout updated
  • Deprecated

    • Legacy exception name preserved with deprecation warning

Review Change Stack

Adds StreamException / StreamApiException / StreamRateLimitException /
StreamTransportException / StreamTaskException per the Server-Side SDK
Error Handling Spec §9.2. APIError envelope parsing now surfaces
unrecoverable, exception_fields, more_info, details, and the raw body on
the new StreamApiException class. 429s build StreamRateLimitException
with retry_after parsed from Retry-After (RFC 7231 §7.1.3 — integer
seconds or HTTP-date, past dates clamp to zero, missing/garbage → None).

httpx.RequestError raised by the sync and async transport paths is
wrapped into StreamTransportException with an error_type enum
(connection_reset / timeout / dns_failure / tls_handshake_failed /
unknown) and the original exception preserved via __cause__. The
classifier walks __cause__/__context__ to detect ssl.SSLError and
socket.gaierror that httpx hides one level down.

Adds Stream.wait_for_task and AsyncStream.wait_for_task that poll
get_task until terminal state. Failed tasks raise StreamTaskException
populated from ErrorResult; timeouts raise StreamTransportException with
error_type='timeout'.

getstream.base.StreamAPIException (capital API) is now an alias for the
new StreamApiException via a module-level __getattr__ that emits
DeprecationWarning. The alias points at the same class object, so
isinstance / except / pytest.raises keep working without changes; it
will be removed one minor cycle after this release.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc97e411-4040-4e3d-bf2f-7c9dab60ce32

📥 Commits

Reviewing files that changed from the base of the PR and between ff08a64 and 75fc093.

📒 Files selected for processing (4)
  • getstream/base.py
  • getstream/exceptions.py
  • getstream/stream.py
  • getstream/tasks.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • getstream/stream.py
  • getstream/tasks.py
  • getstream/exceptions.py
  • getstream/base.py

📝 Walkthrough

Walkthrough

Adds a new exception module with API/transport/task exception types and helpers, integrates wrapping into the base client, adds sync/async task polling utilities and client convenience methods, re-exports exceptions at package level, updates changelog, and includes comprehensive tests.

Changes

Stream SDK Exception Hierarchy and Task Polling

Layer / File(s) Summary
Exception Hierarchy and Error Helpers
getstream/exceptions.py
Defines StreamException, StreamApiException, StreamRateLimitException, StreamTransportException, StreamTaskException, transport error constants, JSON/error-envelope parsing, parse_retry_after, classify_transport_error, build_api_exception, and wrap_transport_error.
Base Client Error Handling Integration
getstream/base.py
Routes non-success responses through build_api_exception(response), chains parse errors via raise ... from, wraps httpx.RequestError with wrap_transport_error, and exposes a deprecated StreamAPIException alias via __getattr__.
Task Polling Utilities
getstream/tasks.py
Provides wait_for_task_sync and wait_for_task_async that poll client.get_task until completed (return), failed (raise StreamTaskException), or timeout (raise StreamTransportException with error_type='timeout'). Exports default poll interval/timeout constants.
Stream Client Task Methods
getstream/stream.py
Adds AsyncStream.wait_for_task and Stream.wait_for_task wrappers that delegate to the task polling helpers with poll_interval and timeout parameters.
Package-level Exception Exports
getstream/__init__.py
Re-exports key exception classes (StreamApiException, StreamException, StreamRateLimitException, StreamTaskException, StreamTransportException) along with Stream/AsyncStream.
Changelog Documentation
CHANGELOG.md
Documents the new exceptions, task-polling APIs, transport error wrapping, changed defaults, and deprecated StreamAPIException alias.
Comprehensive Test Suite
tests/test_exceptions.py
Adds tests for exception hierarchy and fields, build_api_exception parsing and fallback, parse_retry_after behavior, transport error classification/wrapping (unit and e2e sync/async), deprecated alias behavior, and sync/async wait_for_task completed/failed/timeout flows.
Dev Dependency
pyproject.toml
Adds pytest-httpserver>=1.1.5 to the dev dependency group.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BaseClient
  participant Exceptions
  Client->>BaseClient: send HTTP request
  BaseClient-->>Client: httpx.RequestError (network) / non-2xx response
  BaseClient->>Exceptions: wrap_transport_error(err) / build_api_exception(response)
  Exceptions-->>BaseClient: StreamTransportException / StreamApiException (or StreamRateLimitException)
  BaseClient-->>Client: raise Stream* exception (with __cause__ chained)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • slavabobik

🐰 A hierarchy of errors, so clean and neat,
Transport wrapped, exceptions complete,
Tasks polled with patience till done,
Back-compat smiled, deprecation's fun,
SDK exceptions now run!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the two main changes: structured error hierarchy and wait_for_task functionality, which are the primary focus of all changes in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cha-2958-error-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_exceptions.py (1)

44-550: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Refactor tests to fixture-driven, class-grouped structure for guideline compliance.

This module is fully function-based and builds most test objects inline. Please convert shared setup to pytest fixtures and group related tests into test classes.

As per coding guidelines, **/test_*.py: "Use fixtures to inject objects in tests; test client fixtures can use the Stream API client" and "Keep tests well organized and use test classes to group similar tests".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_exceptions.py` around lines 44 - 550, Refactor this module by
extracting repeated setup into pytest fixtures and grouping related tests into
classes: create fixtures for the HTTP helper (_make_response), request helper
(_request), mock clients (_FakeSyncClient, _FakeAsyncClient) and common response
bodies, then move related tests into test classes such as TestApiException
(tests using build_api_exception), TestRetryAfter (parse_retry_after tests),
TestTransportWrapping (classify_transport_error, wrap_transport_error, transport
mock tests), TestWaitForTask (wait_for_task_sync/async using the fake clients),
and TestStreamTaskException; update tests to accept fixtures via parameters and
replace inline construction with fixture usage while keeping assertions and
referencing symbols like build_api_exception, parse_retry_after,
classify_transport_error, wrap_transport_error, wait_for_task_sync,
wait_for_task_async, _make_response, _request, _FakeSyncClient, and
_FakeAsyncClient so tests remain functionally identical but fixture-driven and
class-organized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_exceptions.py`:
- Around line 329-383: Replace the MockTransport-based tests
(test_transport_wrapping_via_mock_transport_sync and
test_transport_wrapping_via_mock_transport_async) with fixture-backed servers:
stop using httpx.MockTransport and instead use a pytest http server fixture
(e.g., pytest-httpserver or httpserver) to simulate the failure modes (for sync
test simulate a connection reset by having the server close the connection
immediately or by pointing the client at a stopped server; for async test
simulate a read timeout by delaying the response beyond the client's timeout).
Update the Stream and AsyncStream instantiation in those tests to use the
fixture server's URL, assert the same StreamTransportException properties
(info.value.error_type and __cause__), and ensure proper cleanup via
client.close() / await client.aclose(); replace any monkeypatched fake
transports with the fixture usage and remove httpx.MockTransport references.

---

Outside diff comments:
In `@tests/test_exceptions.py`:
- Around line 44-550: Refactor this module by extracting repeated setup into
pytest fixtures and grouping related tests into classes: create fixtures for the
HTTP helper (_make_response), request helper (_request), mock clients
(_FakeSyncClient, _FakeAsyncClient) and common response bodies, then move
related tests into test classes such as TestApiException (tests using
build_api_exception), TestRetryAfter (parse_retry_after tests),
TestTransportWrapping (classify_transport_error, wrap_transport_error, transport
mock tests), TestWaitForTask (wait_for_task_sync/async using the fake clients),
and TestStreamTaskException; update tests to accept fixtures via parameters and
replace inline construction with fixture usage while keeping assertions and
referencing symbols like build_api_exception, parse_retry_after,
classify_transport_error, wrap_transport_error, wait_for_task_sync,
wait_for_task_async, _make_response, _request, _FakeSyncClient, and
_FakeAsyncClient so tests remain functionally identical but fixture-driven and
class-organized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8c72a26-f491-47d8-bf64-0634eb49fa22

📥 Commits

Reviewing files that changed from the base of the PR and between 35e84b0 and f9ab013.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • getstream/__init__.py
  • getstream/base.py
  • getstream/exceptions.py
  • getstream/stream.py
  • getstream/tasks.py
  • tests/test_exceptions.py

Comment thread tests/test_exceptions.py Outdated
@mogita mogita changed the title [CHA-2958] Error hierarchy + wait_for_task feat: [CHA-2958] error hierarchy + wait_for_task May 28, 2026
…kets

Honors AGENTS.md "do not use mocks or mock things in general unless you
are asked to do that directly" (also flagged by CodeRabbit on PR #261).

Two transport-wrapping tests previously used httpx.MockTransport. They
now drive real httpx errors:
- Connection-refused goes via a freshly-bound-and-closed loopback port.
- Read-timeout goes via a real pytest-httpserver instance that sleeps
  past the client's request_timeout.

Six wait_for_task tests previously used _Fake* stand-in client + response
classes. They now point a real Stream/AsyncStream client at a pytest-
httpserver that returns get-task JSON in the real wire format
(nanosecond-epoch created_at/updated_at, ErrorResult payload).

Adds pytest-httpserver as a dev dependency.
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.

1 participant