Skip to content

feat: add async HTTP client support - #49

Closed
fathiaoyinloye wants to merge 1 commit into
ShadeProtocol:mainfrom
fathiaoyinloye:feat/async-http-client
Closed

feat: add async HTTP client support#49
fathiaoyinloye wants to merge 1 commit into
ShadeProtocol:mainfrom
fathiaoyinloye:feat/async-http-client

Conversation

@fathiaoyinloye

@fathiaoyinloye fathiaoyinloye commented Jul 24, 2026

Copy link
Copy Markdown

Summary

Add an asynchronous HTTP client counterpart to the existing synchronous client, enabling the SDK to be used cleanly in async frameworks such as FastAPI, Starlette, and Django async views while preserving shared request construction and configuration behavior.

Changes

  • Added _AsyncHTTPClient using httpx.AsyncClient.
  • Mirrored the synchronous client's constructor and request interface.
  • Added async request(...) support with the same response shape as the sync client.
  • Extracted shared request-building logic for URL and header construction to avoid duplication.
  • Added aclose() and async context manager support for proper client cleanup.
  • Updated gateway payment handling to support async cleanup and request flow.
  • Added async transport, cleanup, and context-management test coverage.
  • Added pytest-asyncio and updated the lockfile.
  • Added async client usage documentation to the README.
  • Removed malformed trailing markup in http.py that caused a syntax error.

Test plan

  • Full test suite passes
  • Async HTTP client request behavior tested
  • Async client cleanup and aclose() tested
  • Async context manager usage tested
  • Sync and async request construction behavior verified

Validation: 178 passed

Summary by CodeRabbit

  • New Features

    • Added asynchronous request support using a shared HTTP client.
    • Added explicit cleanup for asynchronous connections, including context-manager support.
    • Added documentation and examples for asynchronous payment processing and direct HTTP requests.
  • Bug Fixes

    • Improved retry handling for connection, timeout, and rate-limit errors.
    • Ensured asynchronous responses match synchronous response behavior.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Async HTTP client migration

Layer / File(s) Summary
Shared requests and retry classification
src/shade/http.py
Request construction is centralized for sync and async clients, while retry classification uses httpx transport and status errors.
Async client lifecycle and request flow
src/shade/http.py, src/shade/gateway.py
AsyncHTTPClient now uses a shared httpx.AsyncClient with lazy creation, cleanup, and context-manager support; Gateway exposes aclose().
Async migration validation and usage guidance
tests/test_rate_limit.py, pyproject.toml, README.md, TODO.md
Tests migrate to httpx mocks, dependencies and async usage documentation are updated, and the implementation checklist records follow-up work.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Gateway
  participant AsyncHTTPClient
  participant httpxAsyncClient
  Gateway->>AsyncHTTPClient: process_payment_async request
  AsyncHTTPClient->>httpxAsyncClient: request with URL, headers, and JSON payload
  httpxAsyncClient-->>AsyncHTTPClient: httpx.Response
  AsyncHTTPClient-->>Gateway: parsed response
  Gateway->>AsyncHTTPClient: aclose()
  AsyncHTTPClient->>httpxAsyncClient: aclose()
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: codebestia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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
Title check ✅ Passed The title accurately summarizes the main change: adding async HTTP client support.
Description check ✅ Passed The description covers the summary, changes, and testing, and is mostly complete despite not strictly following the template.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/shade/http.py (2)

549-568: 🩺 Stability & Availability | 🔵 Trivial

Closing the shared httpx.AsyncClient while requests are in-flight will raise RuntimeError.

aclose()/__aexit__ unconditionally close and null out self._client. If another coroutine is mid-request() on the same AsyncHTTPClient instance (e.g. Gateway.aclose() in gateway.py called concurrently with process_payment_async), httpx raises RuntimeError: The connection pool was closed while ... requests/responses were still in-flight, per httpx's documented client lifecycle semantics. This is a legitimate design point for callers sharing one Gateway/AsyncHTTPClient across concurrent tasks — worth documenting (or guarding with an in-flight request counter) rather than only in 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 `@src/shade/http.py` around lines 549 - 568, Document the concurrency contract
around AsyncHTTPClient.aclose() and __aexit__: callers must not close the client
while requests started through _get_client/request are still in flight,
including shared Gateway usage. Add the guidance to the relevant method or class
docstrings without changing the existing lifecycle behavior.

231-248: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the dead _retry_with_backoff helper.

_retry_with_backoff is defined only in src/shade/http.py and is not referenced elsewhere; the sync and async request paths implement their own retry loops instead.

🤖 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 `@src/shade/http.py` around lines 231 - 248, Remove the unused
_retry_with_backoff function, including its retry loop and docstring, from the
module. Leave the existing synchronous and asynchronous request retry
implementations unchanged.
🤖 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 `@src/shade/http.py`:
- Around line 123-129: Update _is_retryable_transport_error to treat the broader
httpx.NetworkError hierarchy as retryable, while preserving the existing
timeout, built-in transport, and urllib.error.URLError handling. This shared
helper must cover httpx.ReadError, httpx.WriteError, and httpx.CloseError for
both SyncHTTPClient.request and AsyncHTTPClient.request.

---

Nitpick comments:
In `@src/shade/http.py`:
- Around line 549-568: Document the concurrency contract around
AsyncHTTPClient.aclose() and __aexit__: callers must not close the client while
requests started through _get_client/request are still in flight, including
shared Gateway usage. Add the guidance to the relevant method or class
docstrings without changing the existing lifecycle behavior.
- Around line 231-248: Remove the unused _retry_with_backoff function, including
its retry loop and docstring, from the module. Leave the existing synchronous
and asynchronous request retry implementations unchanged.
🪄 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 Plus

Run ID: 5f41ec26-ceae-4c0d-bef1-1f658bf9d043

📥 Commits

Reviewing files that changed from the base of the PR and between 475b970 and e1a466c.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • README.md
  • TODO.md
  • pyproject.toml
  • src/shade/gateway.py
  • src/shade/http.py
  • tests/test_rate_limit.py

Comment thread src/shade/http.py
Comment on lines 123 to 129
def _is_retryable_transport_error(exc: Exception) -> bool:
"""Return True for transient network failures that should be retried."""
if httpx is not None and isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
return True

try:
import aiohttp
except ImportError:
aiohttp = None

if aiohttp is not None and isinstance(
exc,
(
aiohttp.ClientConnectionError,
aiohttp.ClientConnectorError,
aiohttp.ClientOSError,
aiohttp.ServerDisconnectedError,
),
):
if isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
return True

if isinstance(exc, (ConnectionResetError, TimeoutError, urllib.error.URLError)):
return True
return False

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

_is_retryable_transport_error misses common transient httpx network errors.

Only httpx.ConnectError and httpx.TimeoutException are treated as retryable. httpx.ConnectError is a subclass of httpx.NetworkError, which also covers httpx.ReadError, httpx.WriteError, and httpx.CloseError — all genuine transient network failures (e.g. a connection reset mid-response) that will now propagate unretried through both SyncHTTPClient.request and AsyncHTTPClient.request (both call this shared helper). This narrows retry coverage compared to the sync-side urllib.error.URLError catch-all on the same line.

🛡️ Proposed fix
-    if isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
+    if isinstance(exc, (httpx.NetworkError, httpx.TimeoutException)):
         return True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _is_retryable_transport_error(exc: Exception) -> bool:
"""Return True for transient network failures that should be retried."""
if httpx is not None and isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
return True
try:
import aiohttp
except ImportError:
aiohttp = None
if aiohttp is not None and isinstance(
exc,
(
aiohttp.ClientConnectionError,
aiohttp.ClientConnectorError,
aiohttp.ClientOSError,
aiohttp.ServerDisconnectedError,
),
):
if isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
return True
if isinstance(exc, (ConnectionResetError, TimeoutError, urllib.error.URLError)):
return True
return False
def _is_retryable_transport_error(exc: Exception) -> bool:
"""Return True for transient network failures that should be retried."""
if isinstance(exc, (httpx.NetworkError, httpx.TimeoutException)):
return True
if isinstance(exc, (ConnectionResetError, TimeoutError, urllib.error.URLError)):
return True
return False
🤖 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 `@src/shade/http.py` around lines 123 - 129, Update
_is_retryable_transport_error to treat the broader httpx.NetworkError hierarchy
as retryable, while preserving the existing timeout, built-in transport, and
urllib.error.URLError handling. This shared helper must cover httpx.ReadError,
httpx.WriteError, and httpx.CloseError for both SyncHTTPClient.request and
AsyncHTTPClient.request.

@codebestia

Copy link
Copy Markdown
Contributor

Hello @fathiaoyinloye
Please address the coderabbit review

@codebestia

Copy link
Copy Markdown
Contributor

Hello @fathiaoyinloye
What is the update with this?

@codebestia codebestia closed this Aug 3, 2026
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