pub struct HttpClient {
pub base_url: Url,
/* private fields */
}Expand description
Shared HTTP client with base URL, throttle, retry policy and retry config.
This is the common structure used by all API clients to hold the
transport every attempt is sent on, the base URL, and the hooks its send
loop, send, runs on every request.
Fields§
§base_url: UrlBase URL for API requests
Implementations§
Source§impl HttpClient
impl HttpClient
Sourcepub fn with_base_url(&self, base_url: &str) -> Result<Self, ApiError>
pub fn with_base_url(&self, base_url: &str) -> Result<Self, ApiError>
Clone this client, pointed at a different base URL.
The transport (and so its connection pool), throttle (and so its hold), retry policy, retry config, runtime, and concurrency limiter are all shared with the original. Use this to reach a sibling API host that should share the same transport configuration and request budget — a venue whose APIs live on their own subdomains is consumed by one client, and a 429 on one holds them all.
Sharing the concurrency limiter is deliberate: the limit exists to keep Cloudflare from seeing a burst from this process, and that is a per-process concern rather than a per-host one.
§Path prefixes are ignored
Request paths are absolute (/user-pnl), and resolving an absolute
path against a base replaces the base’s path entirely. So a prefix in
base_url is silently dropped, with or without a trailing slash:
http://host + /user-pnl -> http://host/user-pnl
http://host/proxy + /user-pnl -> http://host/user-pnl (prefix gone)
http://host/proxy/ + /user-pnl -> http://host/user-pnl (prefix gone)Point this at a scheme, host, and port — not at a sub-path. Fronting the API with a path-prefixed reverse proxy is not supported.
Sourcepub fn runtime(&self) -> &Arc<dyn Runtime> ⓘ
pub fn runtime(&self) -> &Arc<dyn Runtime> ⓘ
The runtime this client’s waits, clock reads and spawns go through:
the one its builder was given, else the target’s default. Shared with
every with_base_url sibling.
Sourcepub async fn acquire_concurrency(&self) -> Option<OwnedSemaphorePermit>
pub async fn acquire_concurrency(&self) -> Option<OwnedSemaphorePermit>
Acquire a concurrency permit, if a limiter is configured.
The returned permit must be held until the HTTP response has been
received. Dropping the permit releases the concurrency slot.
Returns None when no concurrency limit is set.
send takes one for each attempt. Transitional for any
other caller: no crate calls it since Stories 3.4 to 3.6 moved the
hand-written loops onto send, but tests observe the
permit through it. It is removed from the public API once they can
observe it another way, and docs/s1-removals.md names send.
Sourcepub async fn get_bytes(
&self,
path: &str,
query: &[(String, String)],
) -> Result<Vec<u8>, ApiError>
pub async fn get_bytes( &self, path: &str, query: &[(String, String)], ) -> Result<Vec<u8>, ApiError>
GET a URL and return the raw response body as bytes.
Use this for endpoints that return non-JSON payloads (e.g. application/zip
downloads). It runs on send, so it is throttled, gated,
retried and held as every other request is.
A non-2xx response is ApiError::Response.
§Errors
Returns ApiError on URL-join failure, network errors, or non-2xx
responses.
Source§impl HttpClient
impl HttpClient
Sourcepub async fn health<E: RequestError>(
&self,
path: &str,
costs: &[Cost],
) -> Result<Pong, E>
pub async fn health<E: RequestError>( &self, path: &str, costs: &[Cost], ) -> Result<Pong, E>
Ping path with one GET on send, charging costs.
The ping takes a concurrency permit and the throttle, and is retried
and held by the client’s policy, as every other request is. Its
Pong::round_trip is that of the attempt that answered, so every
venue’s ping latency means the same thing.
§Errors
A final non-2xx response is ApiError::Response as E, and so is a
failure before one (a refused cost, a transport error).
Source§impl HttpClient
impl HttpClient
Sourcepub async fn send(
&self,
parts: RequestParts,
costs: &[Cost],
auth: Option<&DynAuthenticator<'_>>,
) -> Result<Response, ApiError>
pub async fn send( &self, parts: RequestParts, costs: &[Cost], auth: Option<&DynAuthenticator<'_>>, ) -> Result<Response, ApiError>
Send parts, retrying as the client’s policy decides.
Each attempt takes a concurrency permit, waits while the throttle
charges it (Throttle::acquire, with costs), is signed by auth
when one is given, and is sent, within parts.timeout when it is set.
The throttle then sees the response
(Throttle::observe), the policy decides what follows
(RetryPolicy::decide), and a hold the decision carries is applied
(Throttle::hold) whether or not the request is retried.
A retry releases the permit and sleeps the longer of the policy’s wait
and the loop’s own floor, RetryConfig::retry_delay for the attempt.
The floor belongs to the loop, so no policy can retry sooner. The loop
never retries past RetryConfig::max_retries, whatever the policy
says.
Each retry logs a WARN under the polyoxide_core target,
Retriable status <code> on <path>, retry <n> after <ms>ms, where
<ms> is the longer of the retry’s sleep and the hold its decision
set, since the retry waits out both. A hold that is not a retry logs
one too: Status <code> on <path>, not retried: every request held <ms>ms.
Returns the response the policy is done with, whatever its status; one it fails, or would retry with no retry left, is an error.
§Errors
ApiError::Responsewhen the policy fails the last response.ApiError::Urlwhenparts.pathdoes not join the base URL.ApiError::Refusedwhen the throttle refuses a cost, or the signing error whenauthfails. Nothing is sent.ApiError::Transportwhen no response arrives. That attempt is neither observed nor retried.