timvw-rust-runtime
The runtime of Rust clients generated by
openapi-generator-timvw-rust (-g timvw-rust, with the
default httpStack=tower). A generated client sends every request through a Client: a
tower service from http::Request<Body> to http::Response<Body>. That makes everything
around sending a tower layer: retries, timeouts, logging, headers, authentication, rate limits. Use the layers this
crate ships, any layer from the tower ecosystem, or write your own.
- Quick start
- Retries
- Timeouts
- Logging
- Middleware: headers, authentication, layers of your own
- Concurrency and rate limits
- Choosing the HTTP client, and tests without a network
- Your own types, and streams of server-sent events
- Errors
- Features
Quick start
A generated client keeps its Client in Configuration.client, which starts as Client::default(): reqwest, with
retries of 429, 408, 5xx and failed connections. layer wraps it in more:
use Duration;
use TraceLayer;
use Client;
use TimeoutLayer;
let client = default // reqwest, with retries
.layer // logs each call, and each retry
.layer; // bounds the whole call, retries included
// In a generated client:
// let mut configuration = my_client::apis::configuration::Configuration::new();
// configuration.client = client;
Layers apply from the inside out: the last one added sees a request first. Layers added to Client::default()
wrap its retries. To put a layer inside the retries (a timeout or a log line per attempt), or to retry differently,
build the stack from the bare service, Client::from_service(ReqwestService::default()):
use Duration;
use RetryPolicy;
use TraceLayer;
use ;
use RetryLayer;
use TimeoutLayer;
let client = from_service // reqwest, no retries
.layer // each attempt
.layer // logs each attempt
.layer; // retries
A Client is cheap to clone, and clones share their layers' state (a rate limit, a token cache).
Retries
Client::default(), Client::new() and Client::from_reqwest(..) retry with RetryPolicy::default() (feature
retry, on by default). retry::RetryPolicy is a tower::retry::Policy for HTTP APIs:
- It retries
429 Too Many Requests,408 Request Timeout, every5xxand failures to connect. Any other response or error is returned at once. - It waits as long as
Retry-Afterasks (in seconds), or else backs off exponentially: 200 ms, 400 ms, 800 ms, ..., capped at 8 s. The cap applies toRetry-Aftertoo. - It retries at most 3 times by default.
- It does not retry a request whose body is a stream (a streamed file upload), since that cannot be sent twice. JSON, form, text and in-memory multipart bodies are sent again.
use Duration;
use RetryPolicy;
use ;
use RetryLayer;
let policy = new
.base_delay
.max_delay;
let client = from_service.layer;
Without retries: Client::from_service(ReqwestService::default()), or build without the retry feature.
The policy retries every method, POST included: for most APIs a 429 or 503 means the request was not processed.
If a retried POST could act twice on your API, send an idempotency key the API understands, or write a
tower::retry::Policy of your own: the trait has two methods, retry (whether to try again and after which delay)
and clone_request. Body::try_clone clones a body held in memory.
Timeouts
tower::timeout::TimeoutLayer fails a request that takes too long. Where it sits decides what it bounds (see
Quick start). A timeout is an Error whose is_timeout() is true:
use Duration;
use ;
async
let client = default.layer;
reqwest's own timeouts (connect timeout, read timeout) are set on a reqwest::Client, see
Choosing the HTTP client. For a response streamed for minutes (server-sent events), a
total timeout ends the stream; use a connect timeout instead, or none.
Logging
trace::TraceLayer (feature tracing) logs every request with tracing:
- a
http.requestspan withhttp.request.method,url.full(without the query, which can hold an API key) andhttp.response.status_code; - an event when the response arrives, with
statusandelapsed_ms:DEBUGbelow500,WARNfor a5xxor a failure to send; - with
RetryPolicy, aDEBUGeventretryingwithattempt,max_retries,delay_msandstatus.
Headers and bodies are never logged. The target is timvw_rust_runtime::trace:
use TraceLayer;
use Client;
// RUST_LOG=timvw_rust_runtime=debug shows every request, RUST_LOG=timvw_rust_runtime=warn only failures.
fmt
.with_env_filter
.init;
let client = default.layer;
WARN http.request{http.request.method=POST url.full=https://api.example.com/v1/items http.response.status_code=503}: timvw_rust_runtime::trace: response status=503 elapsed_ms=41
DEBUG timvw_rust_runtime::trace: retrying attempt=1 max_retries=3 delay_ms=200 status=503
DEBUG http.request{http.request.method=POST url.full=https://api.example.com/v1/items http.response.status_code=200}: timvw_rust_runtime::trace: response status=200 elapsed_ms=380
reqwest's and hyper's own DEBUG events (connecting, pooling) are logged inside the http.request span too.
For more (headers, custom spans and fields, OpenTelemetry), use tower-http's
TraceLayer; it fits the same way:
let client = default.layer;
Middleware
Anything that changes a request or a response is a layer. Three ways to write one, from the shortest:
A header on every request: tower::util::MapRequestLayer with a function, or tower-http's
SetRequestHeaderLayer:
use ;
use ;
let client = default
.layer
.layer;
Several layers at once: tower::ServiceBuilder composes layers top to bottom (the first one sees a request
first), the reverse of chaining Client::layer:
use Duration;
use RetryPolicy;
use TraceLayer;
use ;
let client = from_service.layer;
A layer of your own, for work that needs state or await, such as fetching and caching an access token. A layer
makes the service; the service changes the request and calls the service inside it:
use Future;
use Pin;
use Arc;
use ;
use ;
use ;
use ;
async
let client = default.layer;
A layer's service must be Clone + Send; it need not be Sync. Its response body can be any http_body::Body.
Concurrency and rate limits
tower::limit::ConcurrencyLimitLayer caps requests in flight; the clones of a Client share the cap.
RateLimit cannot be cloned, so put a Buffer in front of it: the buffer shares one limiter between all clones. A
buffer starts a tokio task, so build that client inside a tokio runtime.
use Duration;
use Client;
async
These layers need tower's limit and buffer features in your Cargo.toml, like timeout for TimeoutLayer:
tower = { version = "0.5", features = ["limit", "buffer", "timeout"] }.
Choosing the HTTP client
Client::from_reqwest sends with a reqwest::Client you built (a proxy, a custom TLS setup, connect and read timeouts,
pool settings), with the default retries; Client::from_service(ReqwestService::new(reqwest_client)) without them:
use Duration;
use Client;
let reqwest_client = builder
.connect_timeout
.proxy
.build
.unwrap;
let client = from_reqwest;
Client::from_service sends with any tower service from http::Request<Body> to http::Response<B>, as is, with no
retries added: a hyper client, a client for another runtime, or a function that answers without a network, for tests:
use ;
let client = from_service;
Without the reqwest feature (and rustls or native-tls), the crate does not depend on reqwest, and
Client::default() fails every request: set a service with Client::from_service.
Your own types and server-sent events
A generated function decodes to the spec's models. To send and decode types of your own (fewer fields, a provider
whose replies differ, serde_json::Value), call the client directly: the same layers apply (retries, logging).
Response::events() reads a text/event-stream body, and .json::<T>() decodes each event's data::
use StreamExt;
use Deserialize;
use ;
async
data: [DONE] or the end of the body ends the stream. A payload that does not decode yields an error whose
is_decode() is true, and the stream goes on. events() alone gives each sse::Event undecoded (event:, data:,
id:), and sse::Decoder is the parser, for a body read some other way. A generated client's typed event streams use
the same parser.
Errors
A request that fails to send is an Error:
| Method | True when |
|---|---|
is_builder() |
the request could not be built (an invalid URL, header or body) |
is_request() |
the service failed; is_timeout() and is_connect() narrow it down |
is_body() |
reading the response body failed |
is_decode() |
the response could not be decoded (JSON) |
source() is the error of the service or layer that failed: a reqwest::Error, tower's Elapsed, or your own. A
generated client wraps it as apis::Error::Http(error). A response with an error status is not an Error here; the
generated client turns it into apis::Error::ResponseError.
Features
| Feature | Default | What it adds |
|---|---|---|
rustls |
yes | reqwest with rustls, as Client::default() |
native-tls |
no | reqwest with the platform's TLS |
reqwest |
via rustls or native-tls |
ReqwestService, Client::from_reqwest |
retry |
yes | retry::RetryPolicy (sleeps with tokio), and retries in Client::default() |
tracing |
no | trace::TraceLayer, and a log event per retry |
A generated crate forwards native-tls, rustls, retry and tracing under the same names.
What the generated code uses
The generated functions build requests with RequestBuilder (query, header, bearer_auth, basic_auth, json,
form, body, multipart, build) and read responses with Response (status, headers, text, bytes,
bytes_stream): the methods of reqwest's types of the same names. multipart::Form encodes multipart bodies.
License
MIT OR Apache-2.0.