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

@fathiaoyinloyefathiaoyinloye 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe title accurately summarizes the main change: adding async HTTP client support.
Description check✅ PassedThe description covers the summary, changes, and testing, and is mostly complete despite not strictly following the template.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadsrc/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."""
ifhttpxisnotNoneandisinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
returnTrue
try:
importaiohttp
exceptImportError:
aiohttp=None
ifaiohttpisnotNoneandisinstance(
exc,
(
aiohttp.ClientConnectionError,
aiohttp.ClientConnectorError,
aiohttp.ClientOSError,
aiohttp.ServerDisconnectedError,
),
):
ifisinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
returnTrue
ifisinstance(exc, (ConnectionResetError, TimeoutError, urllib.error.URLError)):
returnTrue
returnFalse
def_is_retryable_transport_error(exc: Exception) ->bool:
"""Return True for transient network failures that should be retried."""
ifisinstance(exc, (httpx.NetworkError, httpx.TimeoutException)):
returnTrue
ifisinstance(exc, (ConnectionResetError, TimeoutError, urllib.error.URLError)):
returnTrue
returnFalse
🤖 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?

Sign up for freeto 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

@fathiaoyinloye@codebestia