Repository navigation
Make the HTTP client an implementation detail, then decide on requests vs httpx2 #31
Description
requests currently leaks through the public surface of this package in several places, so changing the HTTP library would break callers. The goal here is to mask that first — make the library an implementation detail behind our own types — so the choice between requests and httpx becomes a contained, reversible decision rather than a breaking one.
User count is small, so a clean break in a 3.0 is acceptable.
Where requests leaks today
- Transport errors escape untranslated.
do_request()catches onlyrequests.exceptions.HTTPError. Connection refused, DNS failure, TLS errors, timeouts, too many redirects — all reach the caller asrequests.exceptions.*. Anyone handling them is coupled torequests. - Malformed responses escape as
json.JSONDecodeError. Every method doesjson.loads(response), so a non-JSON 200 (a proxy's HTML error page, say) surfaces as a parsing error rather than an API error. YetiApi.clientis a publicrequests.Session. It's assigned in__init__and replaced inauth_api_key(), and people reach into it — it was the only way to issue a DELETE until Dispatch DELETE in do_request #28.requests_toolbeltis a dependency for one call.upload_dfiq_archive()builds aMultipartEncoderand immediately calls.to_string(), which reads the whole body into memory — so the streaming thatrequests_toolbeltexists for isn't used. Native multipart (files=) is equivalent, in either library.- The tests are coupled to
requests.tests/api.pypatchesyeti.api.requests.Session.*38 times (27post, 5get, 4patch, 1delete, 1send). Any library change rewrites the suite.
What's already library-agnostic, and should stay that way: do_request()'s signature (method, URL, json_data, body, headers, params; returns bytes), the tls_cert constructor argument (a path), and the error types downstream actually uses. yeti-agents catches only YetiApiError and YetiAuthError, and never touches .client.
Phase 1 — mask it (3.0)
- A transport error type. Add
YetiConnectionError(YetiError)for anything that prevents a response — connection, DNS, TLS, redirects — andYetiTimeoutError(YetiConnectionError). Raise themfromthe underlying exception, so the cause stays visible in tracebacks while callers only ever catch yeti types. - Malformed responses become API errors. A body that doesn't parse as the expected JSON raises
YetiApiError(or aYetiResponseErrorsubclass) carrying the status code, notJSONDecodeError. - Make the session private.
client→_session.do_request()is the supported low-level escape hatch, which is whatclientwas being used for. - Default timeouts. There is currently no timeout anywhere, and
requestshas no default, so a server that accepts a connection and stalls hangs every call forever — including the agent's tool calls in yeti-agents. Add a constructor-level default (connect and read), overridable per call. - Drop
requests_toolbelt. Use native multipart inupload_dfiq_archive(). - Test against a transport seam, not a library. Put the actual HTTP call behind one small internal function or class, and give the tests a fake for it. Pass raw request bodies to requests as data #29 is the cautionary tale: patching
Session.post, which accepts any keyword, let abody=thatrequestswould reject pass unnoticed. The fake must reject arguments the real library would, or the tests should mock at the library's lowest seam (requests'Session.send, httpx'sMockTransport).
At the end of phase 1, no requests type appears in any public signature, attribute, docstring or raised exception, and switching libraries touches one module plus the test fake.
Phase 2 — choose the client
Only worth doing once phase 1 is in, and decidable on its own merits.
If it's httpx, target httpx2, not httpx. Plain httpx (encode/httpx) has stayed at 0.28.1, still pre-1.0; the actively released line is httpx2 (github.com/pydantic/httpx2), at 2.13.1. The ecosystem is already moving: authlib now imports httpx2 and treats httpx as a deprecated fallback that it plans to remove.
What it would buy:
- A native async client. yeti-agents is async and currently runs this client in
asyncio.to_thread. The right shape is anAsyncYetiApialongside the sync one, sharing everything but the transport. - HTTP/2, and timeouts by default — though phase 1 adds timeouts regardless.
Behaviour to preserve or document:
- CA bundle environment variables differ.
requestshonoursREQUESTS_CA_BUNDLEandCURL_CA_BUNDLE; httpx usesSSL_CERT_FILEandSSL_CERT_DIR. Users behind TLS-intercepting proxies who rely on the former would fail silently after a swap. Either honour both, or call it out loudly in the release notes. (tls_certis unaffected, since it's explicit.) - Proxies and
.netrcfrom the environment. Both libraries read them by default (trust_env), but check the edge cases rather than assume. - TLS configuration style. httpx is moving towards
ssl.SSLContextobjects rather than path strings forverify, sotls_certwould become a context built internally.
Effort is mostly tests. With phase 1's seam in place, the transport module is a few dozen lines; the bulk is porting the fake and the e2e assumptions.
Versioning
Phase 1 is the breaking release (3.0): client goes away, and callers catching requests exceptions must catch YetiConnectionError instead. Phase 2 needs no further public-API change, so it can be a minor release — except for the CA-bundle environment variables, which must be in the notes either way.
Activity