reqwest-middleware

repository·main·Indexed 18 days ago

https://github.com/truelayer/reqwest-middleware

A wrapper around the reqwest crate that enables the use of middleware chains for HTTP clients. It allows developers to inject cross-cutting concerns such as retries, tracing, and caching into the request lifecycle. The library provides the ClientBuilder to construct a ClientWithMiddleware and defines the Middleware and RequestInitialiser traits for extending request/response logic. Concrete implementations include reqwest-retry and reqwest-tracing.

Tokens
13.1K
Snippets
35
Records
45
Agent score
61%

What's inside reqwest-middleware

  1. Available middleware implementations

    main

    The reqwest-middleware crate provides the framework for middleware chains but does not include implementations itself. The following crates are part of this repository and provide concrete functionality:

    • reqwest-retry: Retries failed requests.
    • reqwest-tracing: Provides tracing integration and optional OpenTelemetry support.
  2. Install reqwest-middleware and related crates

    main

    To use reqwest-middleware, add it along with reqwest and a runtime like tokio to your Cargo.toml. You can also include concrete middleware implementations like reqwest-retry and reqwest-tracing as dependencies.

    # Cargo.toml
    # ...
    [dependencies]
    reqwest = "0.13"
    reqwest-middleware = "0.5"
    reqwest-retry = "0.9"
    reqwest-tracing = "0.6"
    tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
  3. Use reqwest-retry to add retry logic to your client

    main

    To implement retry logic in your reqwest requests, you must build a RetryTransientMiddleware using a RetryPolicy. Once built, attach this middleware to a reqwest_middleware::ClientBuilder.

    For convenience, retry-policies::policies is re-exported under reqwest_retry::policies.

    // Conceptual workflow:
    // 1. Define a RetryPolicy from reqwest_retry::policies
    // 2. Build RetryTransientMiddleware from that policy
    // 3. Attach to reqwest_middleware::ClientBuilder
  4. Disable OpenTelemetry context propagation in TracingMiddleware

    main

    By default, TracingMiddleware propagates OpenTelemetry contexts. If you want to prevent the middleware from injecting trace headers (like traceparent) into outgoing requests, you can disable this behavior by providing the DisableOtelPropagation extension during client initialization.

    Use .with_init(Extension(DisableOtelPropagation)) when building your reqwest_middleware::ClientWithMiddleware.

    use reqwest_middleware::{ClientBuilder, Extension};
    use reqwest_tracing::{TracingMiddleware, DisableOtelPropagation};
    
    let client = ClientBuilder::new(reqwest::Client::new())
        .with_init(Extension(DisableOtelPropagation))
        .with(TracingMiddleware::default())
        .build();
  5. Disable OpenTelemetry context propagation

    main

    If you want to use TracingMiddleware to create local spans for a request but do not want to inject OpenTelemetry tracing headers into the outgoing HTTP request (to avoid propagating the context to downstream services), you can add crate::DisableOtelPropagation to the request extensions.

    This is useful when you want observability for the current client's activity without affecting the downstream trace context.

    use http::Extensions;
    use reqwest_tracing::DisableOtelPropagation;
    
    // Example of how the middleware checks for this extension
    // extensions.get::<DisableOtelPropagation>().is_some() -> skips injection
  6. Implement a custom ReqwestOtelSpanBackend

    main

    For advanced tracing requirements, such as calculating custom metrics (e.g., request elapsed time) or adding custom attributes to spans, implement the ReqwestOtelSpanBackend trait.

    This trait requires two methods:

    • on_request_start: Called when the request begins. Use this to initialize spans (often using the reqwest_otel_span! macro) and store initial state in the request Extensions.
    • on_request_end: Called when the request completes. Use this to record final attributes, handle outcomes (success/failure), and clean up state.

    When using a custom backend, initialize the middleware using TracingMiddleware::<YourBackend>::new().

    use reqwest_middleware::Result;
    use http::Extensions;
    use reqwest::{Request, Response};
    use reqwest_middleware::ClientBuilder;
    use reqwest_tracing::{
        default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend, TracingMiddleware
    };
    use tracing::Span;
    use std::time::{Duration, Instant};
    
    pub struct TimeTrace;
    
    impl ReqwestOtelSpanBackend for TimeTrace {
        fn on_request_start(req: &Request, extension: &mut Extensions) -> Span {
            extension.insert(Instant::now());
            reqwest_otel_span!(name="example-request", req, time_elapsed = tracing::field::Empty)
        }
    
        fn on_request_end(span: &Span, outcome: &Result<Response>, extension: &mut Extensions) {
            let time_elapsed = extension.get::<Instant>().unwrap().elapsed().as_millis() as i64;
            default_on_request_end(span, outcome);
            span.record("time_elapsed", &time_elapsed);
        }
    }
    
    let http = ClientBuilder::new(reqwest::Client::new())
        .with(TracingMiddleware::<TimeTrace>::new())
        .build();
  7. Create a middleware-enabled client with ClientBuilder

    main

    To use middleware with reqwest, wrap a standard reqwest::Client using ClientBuilder. You can attach any type that implements the Middleware trait using the .with() method. Once configured, call .build() to obtain a ClientWithMiddleware. Sending requests with the resulting client is identical to using the standard reqwest::Client.

    use reqwest::{Client, Request, Response};
    use reqwest_middleware::{ClientBuilder, Middleware, Next, Result};
    use http::Extensions;
    
    struct LoggingMiddleware;
    
    #[async_trait::async_trait]
    impl Middleware for LoggingMiddleware {
        async fn handle(
            &self,
            req: Request,
            extensions: &mut Extensions,
            next: Next<'_>,
        ) -> Result<Response> {
            println!("Request started {:?}", req);
            let res = next.run(req, extensions).await;
            println!("Result: {:?}", res);
            res
        }
    }
    
    async fn run() {
        let reqwest_client = Client::builder().build().unwrap();
        let client = ClientBuilder::new(reqwest_client)
            .with(LoggingMiddleware)
            .build();
        let resp = client.get("https://truelayer.com").send().await.unwrap();
        println!("TrueLayer page HTML: {}", resp.text().await.unwrap());
    }
  8. Customize span names with OtelName

    main

    You can customize the OpenTelemetry span names in two ways:

    1. Globally via Client Initialization: Use Extension(OtelName("name")) with .with_init() on the ClientBuilder. This applies the name to all requests made by the client.
    2. Per-Request: Use .with_extension(OtelName("name")) on an individual reqwest request. Per-request extensions take priority over global client extensions.

    Use the OtelName type to wrap the string intended for the span name.

    # use reqwest_middleware::Result;
    use reqwest_middleware::{ClientBuilder, Extension};
    use reqwest_tracing::{
        TracingMiddleware, OtelName
    };
    # async fn example() -> Result<()> {
    let reqwest_client = reqwest::Client::builder().build().unwrap();
    let client = ClientBuilder::new(reqwest_client)
       // Inserts the extension before the request is started
       .with_init(Extension(OtelName("my-client".into())))
       // Makes use of that extension to specify the otel name
       .with(TracingMiddleware::default())
       .build();
    
    let resp = client.get("https://truelayer.com").send().await.unwrap();
    
    // Or specify it on the individual request (will take priority)
    let resp = client.post("https://api.truelayer.com/payment")
        .with_extension(OtelName("POST /payment".into()))
       .send()
       .await
       .unwrap();
    # Ok(())
    # }
  9. Use `RetryTransientMiddleware` to handle transient request failures

    main

    The RetryTransientMiddleware provides retry logic for requests that fail in a transient manner (errors that can be safely retried). It uses a RetryPolicy to determine the wait time between attempts. On non-wasm32 architectures, it uses tokio::time::sleep to respect runtime pauses.

    Important Limitation: Streaming Bodies

    This middleware cannot handle requests with streaming bodies because the Request object must be cloneable to perform retries. If you attempt to use a streaming body, you will receive an Error::Middleware with the message: 'Request object is not cloneable. Are you passing a streaming body?'.

    Workarounds:

    • Use static request bodies (e.g., Body::from(String) or Body::from(Bytes)).
    • Wrap this middleware in a custom one that skips retries for streaming requests.
    • Implement a custom retry middleware that rebuilds streaming requests from the data source.
    use std::time::Duration;
    use reqwest_middleware::ClientBuilder;
    use retry_policies::{RetryDecision, RetryPolicy, Jitter};
    use retry_policies::policies::ExponentialBackoff;
    use reqwest_retry::RetryTransientMiddleware;
    use reqwest::Client;
    
    // Create an ExponentialBackoff retry policy
    let retry_policy = ExponentialBackoff::builder()
        .retry_bounds(Duration::from_secs(1), Duration::from_secs(60))
        .jitter(Jitter::Bounded)
        .base(2)
        .build_with_total_retry_duration(Duration::from_secs(24 * 60 * 60));
    
    // Initialize middleware and add it to the client
    let retry_transient_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
    let client = ClientBuilder::new(Client::new()).with(retry_transient_middleware).build();
  10. Use RetryTransientMiddleware to retry failed HTTP requests

    main

    To implement automatic retries for HTTP requests, use the RetryTransientMiddleware. This middleware uses a RetryPolicy to determine when and how many times a request should be retried. You can integrate it into your client using reqwest_middleware::ClientBuilder.

    use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
    use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
    
    async fn run_retries() {
        // Retry up to 3 times with increasing intervals between attempts.
        let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
        let client = ClientBuilder::new(reqwest::Client::new())
            .with(RetryTransientMiddleware::new_with_policy(retry_policy))
            .build();
    
        client
            .get("https://truelayer.com")
            .header("foo", "bar")
            .send()
            .await
            .unwrap();
    }
  11. Build a ClientWithMiddleware using ClientBuilder

    main

    Use ClientBuilder to construct a ClientWithMiddleware by attaching a stack of middleware and request initialisers. ClientBuilder provides ergonomic methods like with for middleware and with_init for initialisers, which automatically wrap them in an Arc.

    let client = ClientBuilder::new(reqwest::Client::new())
        .with(MyMiddleware)
        .with_init(MyInitialiser)
        .build();