polish(transport): async close cancellation-safe + _delete_with_body params + cycle detector + method.upper hoist#372
Conversation
… detect; method.upper hoist Four small transport/resource polish fixes from the Round-3 audit: - `AsyncTransport.close()` awaits `aclose()` BEFORE flipping `_closed` (#333). The pre-fix order silently no-op'd a follow-up `close()` if the first await was cancelled, orphaning the httpx pool until GC. `httpx.AsyncClient.aclose` is itself idempotent, so dropping the prior flip is safe and means a retried close actually drains the pool. - `SyncResource._delete_with_body` and `AsyncResource._delete_with_body` gain a `params` kwarg threaded through to `transport.request(...)` (#340). Closes the asymmetry with `_delete_with_body_json`, which already accepted it. No public-API change; existing callers default to `params=None`. - `_list_all` cursor-loop detector now tracks the full seen-set (capped at `_CURSOR_SEEN_CAP=1024`) instead of just the previous cursor (#352). Catches `A → B → A → B → ...` cycles caused by LB drift or pagination cache rotation; legitimately very-long paginates still work because the cap degrades the guard to the legacy adjacent-repeat check. - `method.upper()` is hoisted to a `method_upper` local at the top of both `SyncTransport.request` and `AsyncTransport.request` and reused for auth-signing, the httpx call, log statements, and the method- class checks (#342). Mirrors the #262 invariant-header hoist. Closes #333, #340, #342, #352
Code ReviewOverviewFour focused transport/resource fixes from the Round-3 audit. Clean, minimal, well-motivated. Each has a corresponding test and a closed issue. No public API breaks. Overall this is a solid PR — the notes below are mostly minor polish rather than blockers. Fix 1 —
|
| Fix | Verdict |
|---|---|
method.upper() hoist |
✅ Merge as-is |
AsyncTransport.close() cancellation |
✅ Merge; optional docstring note about concurrent-close idempotency |
_delete_with_body params |
✅ Merge as-is |
| Pagination cycle detector | ✅ Merge; consider adding a cap-degradation test as a follow-up issue |
No blockers. The cap-degradation test gap is the only item I'd call out as worth tracking.
Summary
Four small transport / resource polish fixes flagged by the Round-3 audit:
an async-close cancellation leak, a missing
paramskwarg on_delete_with_body, a pagination cycle that the O(1) detector couldn'tcatch, and a
method.upper()call recomputed several times per attemptinside the retry loop. None of these are public-API breaks.
Issues closed
AsyncTransport.close()flips_closed=Truebefore awaitingaclose(), leaking the pool on cancellation._delete_with_bodylacked aparamskwarg that the bytes-fast-path sibling already accepted.method.upper()was recomputed several times per retry attempt in both transports.A → B → A → B → …looped forever.Behavioral changes
AsyncTransport.close()no longer silently no-ops after a cancelledfirst call: the second
close()actually drains the httpx pool._list_allnow raisesKalshiErroron any non-adjacent cursor revisit(LB drift / cache rotation), not just immediate replay. Memory is
bounded by
_CURSOR_SEEN_CAP=1024; once exhausted the guard degradesto the legacy adjacent-repeat check, so legitimately very-long
paginates still work.
SyncResource._delete_with_body/AsyncResource._delete_with_bodynow accept
params: dict[str, Any] | None = Nonefor symmetry with_delete_with_body_json. Default isNone; existing callersunaffected.
Tests
tests/test_async_client.py::TestAsyncTransportContextManager::test_issue_333_async_close_cancellation_safe—injects a
CancelledErrorinside the firstaclose(), confirms_closedstaysFalse, then re-invokesclose()and assertshttpx.AsyncClient.is_closed.tests/test_async_client.py::TestAsyncTransportContextManager::test_issue_342_method_upper_hoisted—passes lowercase verb through
AsyncTransport.request; asserts thewire request is sent as
GET.tests/test_base_helpers.py::TestDeleteWithBodyAcceptsParams— syncparamskwarg survives onto therequest URL.
tests/test_base_helpers.py::TestSyncListAllCursorLoopDetection::test_issue_352_pagination_cycle_detector_non_adjacentand the matching async sibling —
A → B → Asequence now raisesKalshiError; replaces the prior test that asserted the old O(1)behavior.
Source
Round-3 independent audit closure plan, wave
W3.