├── reqwest-retry ├── LICENSE-MIT ├── LICENSE-APACHE ├── .gitignore ├── tests │ └── all │ │ ├── main.rs │ │ ├── helpers │ │ ├── mod.rs │ │ └── simple_server.rs │ │ └── retry.rs ├── src │ ├── retryable.rs │ ├── lib.rs │ ├── retryable_strategy.rs │ └── middleware.rs ├── Cargo.toml ├── README.md └── CHANGELOG.md ├── reqwest-tracing ├── LICENSE-MIT ├── LICENSE-APACHE ├── src │ ├── middleware.rs │ ├── lib.rs │ ├── reqwest_otel_span_macro.rs │ ├── reqwest_otel_span_builder.rs │ └── otel.rs ├── CHANGELOG.md ├── README.md └── Cargo.toml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── audit.yml │ └── ci.yml └── PULL_REQUEST_TEMPLATE.md ├── reqwest-middleware ├── LICENSE-MIT ├── LICENSE-APACHE ├── Cargo.toml ├── src │ ├── lib.rs │ ├── req_init.rs │ ├── middleware.rs │ ├── error.rs │ └── client.rs └── CHANGELOG.md ├── .gitignore ├── Cargo.toml ├── CODE_OF_CONDUCT.md ├── .cargo └── audit.toml ├── LICENSE-MIT ├── CONTRIBUTING.md ├── README.md └── LICENSE-APACHE /reqwest-retry/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /reqwest-tracing/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @TrueLayer/rust-oss 2 | -------------------------------------------------------------------------------- /reqwest-middleware/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /reqwest-retry/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /reqwest-tracing/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /reqwest-middleware/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /reqwest-retry/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /reqwest-retry/tests/all/main.rs: -------------------------------------------------------------------------------- 1 | mod helpers; 2 | mod retry; 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /reqwest-retry/tests/all/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | mod simple_server; 2 | 3 | pub use simple_server::SimpleServer; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # IDE 5 | .idea/ 6 | .vscode/ 7 | 8 | # Rust 9 | Cargo.lock 10 | /target 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "reqwest-middleware", 4 | "reqwest-tracing", 5 | "reqwest-retry", 6 | ] 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project adheres to the Rust Code of Conduct, which [can be found online](https://www.rust-lang.org/conduct.html). 4 | -------------------------------------------------------------------------------- /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | # See https://github.com/rustsec/rustsec/blob/59e1d2ad0b9cbc6892c26de233d4925074b4b97b/cargo-audit/audit.toml.example for example. 2 | 3 | [advisories] 4 | ignore = [ 5 | "RUSTSEC-2020-0159", 6 | ] 7 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | 3 | on: 4 | schedule: 5 | # Runs at 00:00 UTC everyday 6 | - cron: '0 0 * * *' 7 | push: 8 | paths: 9 | - '**/Cargo.toml' 10 | - '**/Cargo.lock' 11 | pull_request: 12 | 13 | jobs: 14 | audit: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | - uses: actions-rs/audit-check@v1 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Motivations 11 | 12 | 16 | 17 | ## Solution 18 | 19 | 20 | 21 | ## Alternatives 22 | 23 | 24 | 25 | ## Additional context 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug description 11 | 12 | 13 | 14 | ## To Reproduce 15 | 16 | 17 | 18 | ## Expected behavior 19 | 20 | 21 | 22 | ## Environment 23 | 24 | 25 | 26 | - OS: [e.g. Windows] 27 | - Rust version [e.g. 1.51.0] 28 | 29 | ## Additional context 30 | 31 | 32 | -------------------------------------------------------------------------------- /reqwest-retry/src/retryable.rs: -------------------------------------------------------------------------------- 1 | use crate::retryable_strategy::{DefaultRetryableStrategy, RetryableStrategy}; 2 | use reqwest_middleware::Error; 3 | 4 | /// Classification of an error/status returned by request. 5 | #[derive(PartialEq, Eq)] 6 | pub enum Retryable { 7 | /// The failure was due to something that might resolve in the future. 8 | Transient, 9 | /// Unresolvable error. 10 | Fatal, 11 | } 12 | 13 | impl Retryable { 14 | /// Try to map a `reqwest` response into `Retryable`. 15 | /// 16 | /// Returns `None` if the response object does not contain any errors. 17 | /// 18 | pub fn from_reqwest_response(res: &Result) -> Option { 19 | DefaultRetryableStrategy.handle(res) 20 | } 21 | } 22 | 23 | impl From<&reqwest::Error> for Retryable { 24 | fn from(_status: &reqwest::Error) -> Retryable { 25 | Retryable::Transient 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 TrueLayer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /reqwest-middleware/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reqwest-middleware" 3 | version = "0.4.2" 4 | authors = ["Rodrigo Gryzinski "] 5 | edition = "2018" 6 | description = "Wrapper around reqwest to allow for client middleware chains." 7 | repository = "https://github.com/TrueLayer/reqwest-middleware" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["reqwest", "http", "middleware"] 10 | categories = ["web-programming::http-client"] 11 | readme = "../README.md" 12 | 13 | [features] 14 | multipart = ["reqwest/multipart"] 15 | json = ["reqwest/json"] 16 | charset = ["reqwest/charset"] 17 | http2 = ["reqwest/http2"] 18 | rustls-tls = ["reqwest/rustls-tls"] 19 | 20 | [dependencies] 21 | anyhow = "1.0.0" 22 | async-trait = "0.1.51" 23 | http = "1.0.0" 24 | reqwest = { version = "0.12.0", default-features = false } 25 | serde = "1.0.106" 26 | thiserror = "2.0" 27 | tower-service = "0.3.0" 28 | 29 | [dev-dependencies] 30 | reqwest = { version = "0.12.0", features = ["rustls-tls"] } 31 | reqwest-retry = { path = "../reqwest-retry" } 32 | reqwest-tracing = { path = "../reqwest-tracing" } 33 | tokio = { version = "1.0.0", features = ["macros", "rt-multi-thread"] } 34 | wiremock = "0.6.0" 35 | -------------------------------------------------------------------------------- /reqwest-retry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reqwest-retry" 3 | version = "0.8.0" 4 | authors = ["Rodrigo Gryzinski "] 5 | edition = "2018" 6 | description = "Retry middleware for reqwest." 7 | repository = "https://github.com/TrueLayer/reqwest-middleware" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["reqwest", "http", "middleware", "retry"] 10 | categories = ["web-programming::http-client"] 11 | 12 | [features] 13 | default = ["tracing"] 14 | tracing = ["dep:tracing"] 15 | 16 | [dependencies] 17 | reqwest-middleware = { version = ">0.3.0, <0.5.0", path = "../reqwest-middleware" } 18 | 19 | anyhow = "1.0.0" 20 | async-trait = "0.1.51" 21 | futures = "0.3.0" 22 | http = "1.0" 23 | reqwest = { version = "0.12.0", default-features = false } 24 | retry-policies = "0.5" 25 | thiserror = "2.0" 26 | tracing = { version = "0.1.26", optional = true } 27 | 28 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 29 | hyper = "1.0" 30 | tokio = { version = "1.6.0", default-features = false, features = ["time"] } 31 | 32 | [target.'cfg(target_arch = "wasm32")'.dependencies] 33 | wasmtimer = "0.4.1" 34 | getrandom = { version = "0.2.0", features = ["js"] } 35 | 36 | [dev-dependencies] 37 | paste = "1.0.0" 38 | tokio = { version = "1.0.0", features = ["full"] } 39 | wiremock = "0.6.0" 40 | futures = "0.3.0" 41 | -------------------------------------------------------------------------------- /reqwest-retry/README.md: -------------------------------------------------------------------------------- 1 | # reqwest-retry 2 | 3 | Retry middleware implementation for 4 | [`reqwest-middleware`](https://crates.io/crates/reqwest-middleware). 5 | 6 | [![Crates.io](https://img.shields.io/crates/v/reqwest-retry.svg)](https://crates.io/crates/reqwest-retry) 7 | [![Docs.rs](https://docs.rs/reqwest-retry/badge.svg)](https://docs.rs/reqwest-retry) 8 | [![CI](https://github.com/TrueLayer/reqwest-middleware/workflows/CI/badge.svg)](https://github.com/TrueLayer/reqwest-middleware/actions) 9 | [![Coverage Status](https://coveralls.io/repos/github/TrueLayer/reqwest-middleware/badge.svg?branch=main&t=UWgSpm)](https://coveralls.io/github/TrueLayer/reqwest-middleware?branch=main) 10 | 11 | ## Overview 12 | 13 | Build `RetryTransientMiddleware` from a `RetryPolicy`, then attach it to a 14 | `reqwest_middleware::ClientBuilder`. 15 | [`retry-policies::policies`](https://crates.io/crates/retry-policies) is reexported under 16 | `reqwest_retry::policies` for convenience. 17 | 18 | See [`reqwest_middleware`](https://docs.rs/reqwest_middleware) for usage with reqwest. 19 | 20 | #### License 21 | 22 | 23 | Licensed under either of Apache License, Version 24 | 2.0 or MIT license at your option. 25 | 26 | 27 |
28 | 29 | 30 | Unless you explicitly state otherwise, any contribution intentionally submitted 31 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 32 | dual licensed as above, without any additional terms or conditions. 33 | 34 | -------------------------------------------------------------------------------- /reqwest-retry/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Middleware to retry failed HTTP requests built on [`reqwest_middleware`]. 2 | //! 3 | //! Use [`RetryTransientMiddleware`] to retry failed HTTP requests. Retry control flow is managed 4 | //! by a [`RetryPolicy`]. 5 | //! 6 | //! ## Example 7 | //! 8 | //! ``` 9 | //! use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; 10 | //! use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff}; 11 | //! 12 | //! async fn run_retries() { 13 | //! // Retry up to 3 times with increasing intervals between attempts. 14 | //! let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); 15 | //! let client = ClientBuilder::new(reqwest::Client::new()) 16 | //! .with(RetryTransientMiddleware::new_with_policy(retry_policy)) 17 | //! .build(); 18 | //! 19 | //! client 20 | //! .get("https://truelayer.com") 21 | //! .header("foo", "bar") 22 | //! .send() 23 | //! .await 24 | //! .unwrap(); 25 | //! } 26 | //! ``` 27 | 28 | mod middleware; 29 | mod retryable; 30 | mod retryable_strategy; 31 | 32 | pub use retry_policies::{policies, Jitter, RetryDecision, RetryPolicy}; 33 | use thiserror::Error; 34 | 35 | pub use middleware::RetryTransientMiddleware; 36 | pub use retryable::Retryable; 37 | pub use retryable_strategy::{ 38 | default_on_request_failure, default_on_request_success, DefaultRetryableStrategy, 39 | RetryableStrategy, 40 | }; 41 | 42 | /// Custom error type to attach the number of retries to the error message. 43 | #[derive(Debug, Error)] 44 | pub enum RetryError { 45 | #[error("Request failed after {retries} retries")] 46 | WithRetries { 47 | retries: u32, 48 | #[source] 49 | err: reqwest_middleware::Error, 50 | }, 51 | #[error(transparent)] 52 | Error(reqwest_middleware::Error), 53 | } 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | First off, thank you for considering contributing to reqwest-middleware. 4 | 5 | If your contribution is not straightforward, please first discuss the change you 6 | wish to make by creating a new issue before making the change. 7 | 8 | ## Reporting issues 9 | 10 | Before reporting an issue on the 11 | [issue tracker](https://github.com/TrueLayer/reqwest-middleware/issues), 12 | please check that it has not already been reported by searching for some related 13 | keywords. 14 | 15 | ## Pull requests 16 | 17 | Try to do one pull request per change. 18 | 19 | ### Updating the changelog 20 | 21 | Update the changes you have made in 22 | [CHANGELOG](https://github.com/TrueLayer/reqwest-middleware/blob/master/reqwest-middlware/CHANGELOG.md) 23 | file under the **Unreleased** section. 24 | 25 | Add the changes of your pull request to one of the following subsections, 26 | depending on the types of changes defined by 27 | [Keep a changelog](https://keepachangelog.com/en/1.0.0/): 28 | 29 | - `Added` for new features. 30 | - `Changed` for changes in existing functionality. 31 | - `Deprecated` for soon-to-be removed features. 32 | - `Removed` for now removed features. 33 | - `Fixed` for any bug fixes. 34 | - `Security` in case of vulnerabilities. 35 | 36 | If the required subsection does not exist yet under **Unreleased**, create it! 37 | 38 | ## Developing 39 | 40 | ### Set up 41 | 42 | This is no different than other Rust projects. 43 | 44 | ```shell 45 | git clone https://github.com/TrueLayer/reqwest-middleware 46 | cd reqwest-middleware 47 | cargo test 48 | ``` 49 | 50 | ### Useful Commands 51 | 52 | - Run Clippy: 53 | 54 | ```shell 55 | cargo clippy 56 | ``` 57 | 58 | - Check to see if there are code formatting issues 59 | 60 | ```shell 61 | cargo fmt --all -- --check 62 | ``` 63 | 64 | - Format the code in the project 65 | 66 | ```shell 67 | cargo fmt --all 68 | ``` 69 | -------------------------------------------------------------------------------- /reqwest-retry/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.8.0] - 2025-11-26 10 | 11 | ### Breaking Changes 12 | 13 | - Updated `retry-policies` (re-exported as `reqwest_retry::policies`) to 0.5. 14 | 15 | ### Changed 16 | 17 | - Updated `thiserror` to `2.0` 18 | 19 | ## [0.7.0] - 2024-11-08 20 | 21 | ### Breaking changes 22 | - Errors are now reported as `RetryError` that adds the number of retries to the error chain if there were any. This changes the returned error types. 23 | 24 | ### Added 25 | - Added support reqwest-middleware `0.4` next to `0.3` 26 | 27 | ## [0.6.1] - 2024-08-08 28 | 29 | ### Added 30 | - Removed dependency on `chrono` ([#170](https://github.com/TrueLayer/reqwest-middleware/pull/170)) 31 | 32 | ## [0.6.0] - 2024-06-28 33 | 34 | ### Added 35 | - Added `with_retry_log_level` to `RetryTransientMiddleware` 36 | 37 | ### Changed 38 | - Upgraded `retry-policies` to `0.4.0`. 39 | 40 | ## [0.5.0] - 2024-04-10 41 | 42 | ### Breaking changes 43 | - Upgraded `reqwest-middleware` to `0.3.0`. 44 | 45 | ## [0.3.0] - 2023-09-07 46 | ### Changed 47 | - `retry-policies` upgraded to 0.2.0 48 | 49 | ## [0.2.3] - 2023-08-30 50 | ### Added 51 | - `RetryableStrategy` which allows for custom retry decisions based on the response that a request got 52 | 53 | ## [0.2.1] - 2022-12-01 54 | 55 | ### Changed 56 | - Classify `io::Error`s and `hyper::Error(Canceled)` as transient 57 | 58 | ## [0.2.0] - 2022-11-15 59 | ### Changed 60 | - Updated `reqwest-middleware` to `0.2.0` 61 | 62 | ## [0.1.4] - 2022-02-21 63 | ### Changed 64 | - Updated `reqwest-middleware` to `0.1.5` 65 | 66 | ## [0.1.3] - 2022-01-24 67 | ### Changed 68 | - Updated `reqwest-middleware` to `0.1.4` 69 | 70 | ## [0.1.2] - 2021-09-28 71 | ### Added 72 | - Re-export `RetryPolicy` from the crate root. 73 | ### Changed 74 | - Disabled default features on `reqwest` 75 | - Replaced `truelayer-extensions` with `task-local-extensions` 76 | - Updated `reqwest-middleware` to `0.1.2` 77 | 78 | ## [0.1.1] - 2021-09-15 79 | ### Changed 80 | - Updated `reqwest-middleware` dependency to `0.1.1`. 81 | -------------------------------------------------------------------------------- /reqwest-middleware/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides [`ClientWithMiddleware`], a wrapper around [`reqwest::Client`] with the 2 | //! ability to attach middleware which runs on every request. 3 | //! 4 | //! You'll want to instantiate [`ClientWithMiddleware`] using [`ClientBuilder`], then you can 5 | //! attach your middleware using [`with`], finalize it with [`build`] and from then on sending 6 | //! requests is the same as with reqwest: 7 | //! 8 | //! ``` 9 | //! use reqwest::{Client, Request, Response}; 10 | //! use reqwest_middleware::{ClientBuilder, Middleware, Next, Result}; 11 | //! use http::Extensions; 12 | //! 13 | //! struct LoggingMiddleware; 14 | //! 15 | //! #[async_trait::async_trait] 16 | //! impl Middleware for LoggingMiddleware { 17 | //! async fn handle( 18 | //! &self, 19 | //! req: Request, 20 | //! extensions: &mut Extensions, 21 | //! next: Next<'_>, 22 | //! ) -> Result { 23 | //! println!("Request started {:?}", req); 24 | //! let res = next.run(req, extensions).await; 25 | //! println!("Result: {:?}", res); 26 | //! res 27 | //! } 28 | //! } 29 | //! 30 | //! async fn run() { 31 | //! let reqwest_client = Client::builder().build().unwrap(); 32 | //! let client = ClientBuilder::new(reqwest_client) 33 | //! .with(LoggingMiddleware) 34 | //! .build(); 35 | //! let resp = client.get("https://truelayer.com").send().await.unwrap(); 36 | //! println!("TrueLayer page HTML: {}", resp.text().await.unwrap()); 37 | //! } 38 | //! ``` 39 | //! 40 | //! [`build`]: ClientBuilder::build 41 | //! [`ClientBuilder`]: ClientBuilder 42 | //! [`ClientWithMiddleware`]: ClientWithMiddleware 43 | //! [`with`]: ClientBuilder::with 44 | 45 | // Test README examples without overriding module docs. 46 | // We want to keep the in-code docs separate as those allow for automatic linking to crate 47 | // documentation. 48 | #[doc = include_str!("../../README.md")] 49 | #[cfg(doctest)] 50 | pub struct ReadmeDoctests; 51 | 52 | mod client; 53 | mod error; 54 | mod middleware; 55 | mod req_init; 56 | 57 | pub use client::{ClientBuilder, ClientWithMiddleware, RequestBuilder}; 58 | pub use error::{Error, Result}; 59 | pub use middleware::{Middleware, Next}; 60 | pub use req_init::{Extension, RequestInitialiser}; 61 | pub use reqwest; 62 | -------------------------------------------------------------------------------- /reqwest-tracing/src/middleware.rs: -------------------------------------------------------------------------------- 1 | use http::Extensions; 2 | use reqwest::{Request, Response}; 3 | use reqwest_middleware::{Middleware, Next, Result}; 4 | use tracing::Instrument; 5 | 6 | use crate::{DefaultSpanBackend, ReqwestOtelSpanBackend}; 7 | 8 | /// Middleware for tracing requests using the current Opentelemetry Context. 9 | pub struct TracingMiddleware { 10 | span_backend: std::marker::PhantomData, 11 | } 12 | 13 | impl TracingMiddleware { 14 | pub fn new() -> TracingMiddleware { 15 | TracingMiddleware { 16 | span_backend: Default::default(), 17 | } 18 | } 19 | } 20 | 21 | impl Clone for TracingMiddleware { 22 | fn clone(&self) -> Self { 23 | Self::new() 24 | } 25 | } 26 | 27 | impl Default for TracingMiddleware { 28 | fn default() -> Self { 29 | TracingMiddleware::new() 30 | } 31 | } 32 | 33 | #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] 34 | #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] 35 | impl Middleware for TracingMiddleware 36 | where 37 | ReqwestOtelSpan: ReqwestOtelSpanBackend + Sync + Send + 'static, 38 | { 39 | async fn handle( 40 | &self, 41 | req: Request, 42 | extensions: &mut Extensions, 43 | next: Next<'_>, 44 | ) -> Result { 45 | let request_span = ReqwestOtelSpan::on_request_start(&req, extensions); 46 | 47 | let outcome_future = async { 48 | #[cfg(any( 49 | feature = "opentelemetry_0_20", 50 | feature = "opentelemetry_0_21", 51 | feature = "opentelemetry_0_22", 52 | feature = "opentelemetry_0_23", 53 | feature = "opentelemetry_0_24", 54 | feature = "opentelemetry_0_25", 55 | feature = "opentelemetry_0_26", 56 | feature = "opentelemetry_0_27", 57 | feature = "opentelemetry_0_28", 58 | feature = "opentelemetry_0_29", 59 | feature = "opentelemetry_0_30", 60 | ))] 61 | let req = if extensions.get::().is_none() { 62 | // Adds tracing headers to the given request to propagate the OpenTelemetry context to downstream revivers of the request. 63 | // Spans added by downstream consumers will be part of the same trace. 64 | crate::otel::inject_opentelemetry_context_into_request(req) 65 | } else { 66 | req 67 | }; 68 | 69 | // Run the request 70 | let outcome = next.run(req, extensions).await; 71 | ReqwestOtelSpan::on_request_end(&request_span, &outcome, extensions); 72 | outcome 73 | }; 74 | 75 | outcome_future.instrument(request_span.clone()).await 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /reqwest-middleware/src/req_init.rs: -------------------------------------------------------------------------------- 1 | use crate::RequestBuilder; 2 | 3 | /// When attached to a [`ClientWithMiddleware`] (generally using [`with_init`]), it is run 4 | /// whenever the client starts building a request, in the order it was attached. 5 | /// 6 | /// # Example 7 | /// 8 | /// ``` 9 | /// use reqwest_middleware::{RequestInitialiser, RequestBuilder}; 10 | /// 11 | /// struct AuthInit; 12 | /// 13 | /// impl RequestInitialiser for AuthInit { 14 | /// fn init(&self, req: RequestBuilder) -> RequestBuilder { 15 | /// req.bearer_auth("my_auth_token") 16 | /// } 17 | /// } 18 | /// ``` 19 | /// 20 | /// [`ClientWithMiddleware`]: crate::ClientWithMiddleware 21 | /// [`with_init`]: crate::ClientBuilder::with_init 22 | pub trait RequestInitialiser: 'static + Send + Sync { 23 | fn init(&self, req: RequestBuilder) -> RequestBuilder; 24 | } 25 | 26 | impl RequestInitialiser for F 27 | where 28 | F: Send + Sync + 'static + Fn(RequestBuilder) -> RequestBuilder, 29 | { 30 | fn init(&self, req: RequestBuilder) -> RequestBuilder { 31 | (self)(req) 32 | } 33 | } 34 | 35 | /// A middleware that inserts the value into the [`Extensions`](http::Extensions) during the call. 36 | /// 37 | /// This is a good way to inject extensions to middleware deeper in the stack 38 | /// 39 | /// ``` 40 | /// use reqwest::{Client, Request, Response}; 41 | /// use reqwest_middleware::{ClientBuilder, Middleware, Next, Result, Extension}; 42 | /// use http::Extensions; 43 | /// 44 | /// #[derive(Clone)] 45 | /// struct LogName(&'static str); 46 | /// struct LoggingMiddleware; 47 | /// 48 | /// #[async_trait::async_trait] 49 | /// impl Middleware for LoggingMiddleware { 50 | /// async fn handle( 51 | /// &self, 52 | /// req: Request, 53 | /// extensions: &mut Extensions, 54 | /// next: Next<'_>, 55 | /// ) -> Result { 56 | /// // get the log name or default to "unknown" 57 | /// let name = extensions 58 | /// .get() 59 | /// .map(|&LogName(name)| name) 60 | /// .unwrap_or("unknown"); 61 | /// println!("[{name}] Request started {req:?}"); 62 | /// let res = next.run(req, extensions).await; 63 | /// println!("[{name}] Result: {res:?}"); 64 | /// res 65 | /// } 66 | /// } 67 | /// 68 | /// async fn run() { 69 | /// let reqwest_client = Client::builder().build().unwrap(); 70 | /// let client = ClientBuilder::new(reqwest_client) 71 | /// .with_init(Extension(LogName("my-client"))) 72 | /// .with(LoggingMiddleware) 73 | /// .build(); 74 | /// let resp = client.get("https://truelayer.com").send().await.unwrap(); 75 | /// println!("TrueLayer page HTML: {}", resp.text().await.unwrap()); 76 | /// } 77 | /// ``` 78 | pub struct Extension(pub T); 79 | 80 | impl RequestInitialiser for Extension { 81 | fn init(&self, req: RequestBuilder) -> RequestBuilder { 82 | req.with_extension(self.0.clone()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /reqwest-middleware/src/middleware.rs: -------------------------------------------------------------------------------- 1 | use http::Extensions; 2 | use reqwest::{Client, Request, Response}; 3 | 4 | use crate::error::{Error, Result}; 5 | 6 | use std::sync::Arc; 7 | 8 | /// When attached to a [`ClientWithMiddleware`] (generally using [`with`]), middleware is run 9 | /// whenever the client issues a request, in the order it was attached. 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use reqwest::{Client, Request, Response}; 15 | /// use reqwest_middleware::{ClientBuilder, Middleware, Next, Result}; 16 | /// use http::Extensions; 17 | /// 18 | /// struct TransparentMiddleware; 19 | /// 20 | /// #[async_trait::async_trait] 21 | /// impl Middleware for TransparentMiddleware { 22 | /// async fn handle( 23 | /// &self, 24 | /// req: Request, 25 | /// extensions: &mut Extensions, 26 | /// next: Next<'_>, 27 | /// ) -> Result { 28 | /// next.run(req, extensions).await 29 | /// } 30 | /// } 31 | /// ``` 32 | /// 33 | /// [`ClientWithMiddleware`]: crate::ClientWithMiddleware 34 | /// [`with`]: crate::ClientBuilder::with 35 | #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] 36 | #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] 37 | pub trait Middleware: 'static + Send + Sync { 38 | /// Invoked with a request before sending it. If you want to continue processing the request, 39 | /// you should explicitly call `next.run(req, extensions)`. 40 | /// 41 | /// If you need to forward data down the middleware stack, you can use the `extensions` 42 | /// argument. 43 | async fn handle( 44 | &self, 45 | req: Request, 46 | extensions: &mut Extensions, 47 | next: Next<'_>, 48 | ) -> Result; 49 | } 50 | 51 | #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] 52 | #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] 53 | impl Middleware for F 54 | where 55 | F: Send 56 | + Sync 57 | + 'static 58 | + for<'a> Fn(Request, &'a mut Extensions, Next<'a>) -> BoxFuture<'a, Result>, 59 | { 60 | async fn handle( 61 | &self, 62 | req: Request, 63 | extensions: &mut Extensions, 64 | next: Next<'_>, 65 | ) -> Result { 66 | (self)(req, extensions, next).await 67 | } 68 | } 69 | 70 | /// Next encapsulates the remaining middleware chain to run in [`Middleware::handle`]. You can 71 | /// forward the request down the chain with [`run`]. 72 | /// 73 | /// [`Middleware::handle`]: Middleware::handle 74 | /// [`run`]: Self::run 75 | #[derive(Clone)] 76 | pub struct Next<'a> { 77 | client: &'a Client, 78 | middlewares: &'a [Arc], 79 | } 80 | 81 | #[cfg(not(target_arch = "wasm32"))] 82 | pub type BoxFuture<'a, T> = std::pin::Pin + Send + 'a>>; 83 | #[cfg(target_arch = "wasm32")] 84 | pub type BoxFuture<'a, T> = std::pin::Pin + 'a>>; 85 | 86 | impl<'a> Next<'a> { 87 | pub(crate) fn new(client: &'a Client, middlewares: &'a [Arc]) -> Self { 88 | Next { 89 | client, 90 | middlewares, 91 | } 92 | } 93 | 94 | pub fn run( 95 | mut self, 96 | req: Request, 97 | extensions: &'a mut Extensions, 98 | ) -> BoxFuture<'a, Result> { 99 | if let Some((current, rest)) = self.middlewares.split_first() { 100 | self.middlewares = rest; 101 | current.handle(req, extensions, self) 102 | } else { 103 | Box::pin(async move { self.client.execute(req).await.map_err(Error::from) }) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reqwest-middleware 2 | 3 | A crate implementing a wrapper around [reqwest](https://crates.io/crates/reqwest) 4 | to allow for client middleware chains. 5 | 6 | [![Crates.io](https://img.shields.io/crates/v/reqwest-middleware.svg)](https://crates.io/crates/reqwest-middleware) 7 | [![Docs.rs](https://docs.rs/reqwest-middleware/badge.svg)](https://docs.rs/reqwest-middleware) 8 | [![CI](https://github.com/TrueLayer/reqwest-middleware/workflows/CI/badge.svg)](https://github.com/TrueLayer/reqwest-middleware/actions) 9 | [![Coverage Status](https://coveralls.io/repos/github/TrueLayer/reqwest-middleware/badge.svg?branch=main&t=YKhONc)](https://coveralls.io/github/TrueLayer/reqwest-middleware?branch=main) 10 | 11 | This crate provides functionality for building and running middleware but no middleware 12 | implementations. This repository also contains a couple of useful concrete middleware crates: 13 | 14 | * [`reqwest-retry`](https://crates.io/crates/reqwest-retry): retry failed requests. 15 | * [`reqwest-tracing`](https://crates.io/crates/reqwest-tracing): 16 | [`tracing`](https://crates.io/crates/tracing) integration, optional opentelemetry support. 17 | 18 | Note about browser support: automated tests targeting wasm are disabled. The crate may work with 19 | wasm but wasm support is unmaintained. PRs improving wasm are still welcome but you'd need to 20 | reintroduce the tests and get them passing before we'd merge it (see 21 | https://github.com/TrueLayer/reqwest-middleware/pull/105). 22 | 23 | ## Overview 24 | 25 | The `reqwest-middleware` client exposes the same interface as a plain `reqwest` client, but 26 | `ClientBuilder` exposes functionality to attach middleware: 27 | 28 | ```toml 29 | # Cargo.toml 30 | # ... 31 | [dependencies] 32 | reqwest = { version = "0.12", features = ["rustls-tls"] } 33 | reqwest-middleware = "0.4" 34 | reqwest-retry = "0.7" 35 | reqwest-tracing = "0.5" 36 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 37 | ``` 38 | 39 | ```rust 40 | use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; 41 | use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff}; 42 | use reqwest_tracing::TracingMiddleware; 43 | 44 | #[tokio::main] 45 | async fn main() { 46 | // Retry up to 3 times with increasing intervals between attempts. 47 | let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); 48 | let client = ClientBuilder::new(reqwest::Client::new()) 49 | // Trace HTTP requests. See the tracing crate to make use of these traces. 50 | .with(TracingMiddleware::default()) 51 | // Retry failed requests. 52 | .with(RetryTransientMiddleware::new_with_policy(retry_policy)) 53 | .build(); 54 | run(client).await; 55 | } 56 | 57 | async fn run(client: ClientWithMiddleware) { 58 | client 59 | .get("https://truelayer.com") 60 | .header("foo", "bar") 61 | .send() 62 | .await 63 | .unwrap(); 64 | } 65 | ``` 66 | 67 | #### License 68 | 69 | 70 | Licensed under either of Apache License, Version 71 | 2.0 or MIT license at your option. 72 | 73 | 74 |
75 | 76 | 77 | Unless you explicitly state otherwise, any contribution intentionally submitted 78 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 79 | dual licensed as above, without any additional terms or conditions. 80 | 81 | 82 | ## Third-party middleware 83 | 84 | The following third-party middleware use `reqwest-middleware`: 85 | 86 | - [`reqwest-conditional-middleware`](https://github.com/oxidecomputer/reqwest-conditional-middleware) - Per-request basis middleware 87 | - [`http-cache`](https://github.com/06chaynes/http-cache) - HTTP caching rules 88 | - [`reqwest-cache`](https://gitlab.com/famedly/company/backend/libraries/reqwest-cache) - HTTP caching 89 | - [`aliri_reqwest`](https://github.com/neoeinstein/aliri/tree/main/aliri_reqwest) - Background token management and renewal 90 | - [`http-signature-normalization-reqwest`](https://crates.io/crates/http-signature-normalization-reqwest) (not free software) - HTTP Signatures 91 | - [`reqwest-chain`](https://github.com/tommilligan/reqwest-chain) - Apply custom criteria to any reqwest response, deciding when and how to retry. 92 | -------------------------------------------------------------------------------- /reqwest-middleware/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Changed 10 | 11 | - Updated `thiserror` to `2.0` 12 | 13 | ## [0.4.2] - 2025-04-08 14 | 15 | ### Added 16 | - Deprecated `fetch_mode_no_cors` as it's been deprecated in reqwest. 17 | 18 | ## [0.4.1] - 2025-02-24 19 | 20 | - Fixed wasm32 by disabling incompatible parts. On that target, `ClientWithMiddleware` is no longer 21 | a Tower service and has no `ClientWithMiddleware::timeout` function. 22 | 23 | ### Changed 24 | - Updated `wasm-timer` to `wasmtimer` 25 | 26 | ## [0.4.0] - 2024-11-08 27 | 28 | ### Breaking Changes 29 | - `request_middleware::Error` is now a transparent error enum and doesn't add its own context anymore. 30 | 31 | ## [0.3.3] - 2024-07-08 32 | 33 | ### Added 34 | - Implemented `Default` on `ClientWithMiddleware` ([#179](https://github.com/TrueLayer/reqwest-middleware/pull/179)) 35 | 36 | ## [0.3.2] - 2024-06-28 37 | 38 | ### Added 39 | - Added re-export of `reqwest`. 40 | - `http2`, `rustls-tls`, and `charset` features, which simply enable those features in `reqwest`. 41 | 42 | ## [0.3.1] 43 | 44 | ### Fixed 45 | - Included license files in crates 46 | - Fix logging of User-Agent header in reqwest-tracing 47 | 48 | ### Added 49 | - Added `with_retry_log_level` to `RetryTransientMiddleware` in reqwest-retry 50 | - Added `ClientBuilder::from_client` 51 | 52 | ## [0.3.0] - 2024-04-10 53 | 54 | ### Breaking changes 55 | - Upgraded `reqwest` to `0.12.0` 56 | * Removed default-features `json` and `multipart` from `reqwest` dependency 57 | * Added `json` and `multipart` features to `reqwest-middleware` 58 | - Upgraded `matchit` to `0.8.0` 59 | * You may need to update some matches that look like `/a/:some_var` to `/a/{some_var}` 60 | - Removed `task_local_extensions` in favour of `http::Extensions` 61 | * All extensions must be `Clone` now. 62 | 63 | ### Changed 64 | - `RequestBuilder::try_clone` now clones the extensions. 65 | 66 | ### Added 67 | - Implemented `Service` for `ClientWithMiddleware` to have more feature parity with `reqwest`. 68 | - Added more methods like `build_split` to have more feature parity with `reqwest.` 69 | - Added more documentation 70 | 71 | ### [0.2.5] - 2024-03-15 72 | 73 | ### Changed 74 | - Updated minimum version of `reqwest` to `0.11.10`. url_mut, with_url, without_url functions are added after `0.11.10`. 75 | 76 | ### [0.2.4] - 2023-09-21 77 | 78 | ### Added 79 | - Added `fetch_mode_no_cors` method to `reqwest_middleware::RequestBuilder` 80 | 81 | ## [0.2.3] - 2023-08-07 82 | 83 | ### Added 84 | - Added all `reqwest::Error` methods for `reqwest_middleware::Error` 85 | 86 | ## [0.2.2] - 2023-05-11 87 | 88 | ### Added 89 | - `RequestBuilder::version` method to configure the HTTP version 90 | 91 | ## [0.2.1] - 2023-03-09 92 | 93 | ### Added 94 | - Support for `wasm32-unknown-unknown` 95 | 96 | ## [0.2.0] - 2022-11-15 97 | 98 | ### Changed 99 | - `RequestBuilder::try_clone` has a fixed function signature now 100 | 101 | ### Removed 102 | - `RequestBuilder::send_with_extensions` - use `RequestBuilder::with_extensions` + `RequestBuilder::send` instead. 103 | 104 | ### Added 105 | - Implementation of `Debug` trait for `RequestBuilder`. 106 | - A new `RequestInitialiser` trait that can be added to `ClientWithMiddleware` 107 | - A new `Extension` initialiser that adds extensions to each request 108 | - Adds `with_extension` method functionality to `RequestBuilder` that can add extensions for the `send` method to use. 109 | 110 | ## [0.1.6] - 2022-04-21 111 | 112 | Absolutely nothing changed 113 | 114 | ## [0.1.5] - 2022-02-21 115 | 116 | ### Added 117 | - Added support for `opentelemetry` version `0.17`. 118 | 119 | ## [0.1.4] - 2022-01-24 120 | 121 | ### Changed 122 | - Made `Debug` impl for `ClientWithExtensions` non-exhaustive. 123 | 124 | ## [0.1.3] - 2021-10-18 125 | 126 | ### Security 127 | - remove time v0.1 dependency 128 | 129 | ### Fixed 130 | - Handle the `hyper::Error(IncompleteMessage)` as a `Retryable::Transient`. 131 | 132 | ## [0.1.2] - 2021-09-28 133 | ### Changed 134 | - Disabled default features on `reqwest` 135 | - Replaced `truelayer-extensions` with `task-local-extensions` 136 | 137 | ## [0.1.1] 138 | ### Added 139 | - New methods on `ClientWithExtensions` and `RequestBuilder` for sending requests with initial extensions. 140 | -------------------------------------------------------------------------------- /reqwest-middleware/src/error.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{StatusCode, Url}; 2 | use thiserror::Error; 3 | 4 | pub type Result = std::result::Result; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | /// There was an error running some middleware 9 | #[error(transparent)] 10 | Middleware(#[from] anyhow::Error), 11 | /// Error from the underlying reqwest client 12 | #[error(transparent)] 13 | Reqwest(#[from] reqwest::Error), 14 | } 15 | 16 | impl Error { 17 | pub fn middleware(err: E) -> Self 18 | where 19 | E: 'static + Send + Sync + std::error::Error, 20 | { 21 | Error::Middleware(err.into()) 22 | } 23 | 24 | /// Returns a possible URL related to this error. 25 | pub fn url(&self) -> Option<&Url> { 26 | match self { 27 | Error::Middleware(_) => None, 28 | Error::Reqwest(e) => e.url(), 29 | } 30 | } 31 | 32 | /// Returns a mutable reference to the URL related to this error. 33 | /// 34 | /// This is useful if you need to remove sensitive information from the URL 35 | /// (e.g. an API key in the query), but do not want to remove the URL 36 | /// entirely. 37 | pub fn url_mut(&mut self) -> Option<&mut Url> { 38 | match self { 39 | Error::Middleware(_) => None, 40 | Error::Reqwest(e) => e.url_mut(), 41 | } 42 | } 43 | 44 | /// Adds a url related to this error (overwriting any existing). 45 | pub fn with_url(self, url: Url) -> Self { 46 | match self { 47 | Error::Middleware(_) => self, 48 | Error::Reqwest(e) => e.with_url(url).into(), 49 | } 50 | } 51 | 52 | /// Strips the related URL from this error (if, for example, it contains 53 | /// sensitive information). 54 | pub fn without_url(self) -> Self { 55 | match self { 56 | Error::Middleware(_) => self, 57 | Error::Reqwest(e) => e.without_url().into(), 58 | } 59 | } 60 | 61 | /// Returns true if the error is from any middleware. 62 | pub fn is_middleware(&self) -> bool { 63 | match self { 64 | Error::Middleware(_) => true, 65 | Error::Reqwest(_) => false, 66 | } 67 | } 68 | 69 | /// Returns true if the error is from a type `Builder`. 70 | pub fn is_builder(&self) -> bool { 71 | match self { 72 | Error::Middleware(_) => false, 73 | Error::Reqwest(e) => e.is_builder(), 74 | } 75 | } 76 | 77 | /// Returns true if the error is from a `RedirectPolicy`. 78 | pub fn is_redirect(&self) -> bool { 79 | match self { 80 | Error::Middleware(_) => false, 81 | Error::Reqwest(e) => e.is_redirect(), 82 | } 83 | } 84 | 85 | /// Returns true if the error is from `Response::error_for_status`. 86 | pub fn is_status(&self) -> bool { 87 | match self { 88 | Error::Middleware(_) => false, 89 | Error::Reqwest(e) => e.is_status(), 90 | } 91 | } 92 | 93 | /// Returns true if the error is related to a timeout. 94 | pub fn is_timeout(&self) -> bool { 95 | match self { 96 | Error::Middleware(_) => false, 97 | Error::Reqwest(e) => e.is_timeout(), 98 | } 99 | } 100 | 101 | /// Returns true if the error is related to the request. 102 | pub fn is_request(&self) -> bool { 103 | match self { 104 | Error::Middleware(_) => false, 105 | Error::Reqwest(e) => e.is_request(), 106 | } 107 | } 108 | 109 | #[cfg(not(target_arch = "wasm32"))] 110 | /// Returns true if the error is related to connect. 111 | pub fn is_connect(&self) -> bool { 112 | match self { 113 | Error::Middleware(_) => false, 114 | Error::Reqwest(e) => e.is_connect(), 115 | } 116 | } 117 | 118 | /// Returns true if the error is related to the request or response body. 119 | pub fn is_body(&self) -> bool { 120 | match self { 121 | Error::Middleware(_) => false, 122 | Error::Reqwest(e) => e.is_body(), 123 | } 124 | } 125 | 126 | /// Returns true if the error is related to decoding the response's body. 127 | pub fn is_decode(&self) -> bool { 128 | match self { 129 | Error::Middleware(_) => false, 130 | Error::Reqwest(e) => e.is_decode(), 131 | } 132 | } 133 | 134 | /// Returns the status code, if the error was generated from a response. 135 | pub fn status(&self) -> Option { 136 | match self { 137 | Error::Middleware(_) => None, 138 | Error::Reqwest(e) => e.status(), 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /reqwest-tracing/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Opentracing middleware implementation for [`reqwest_middleware`]. 2 | //! 3 | //! Attach [`TracingMiddleware`] to your client to automatically trace HTTP requests. 4 | //! 5 | //! The simplest possible usage: 6 | //! ```no_run 7 | //! # use reqwest_middleware::Result; 8 | //! use reqwest_middleware::{ClientBuilder}; 9 | //! use reqwest_tracing::TracingMiddleware; 10 | //! 11 | //! # async fn example() -> Result<()> { 12 | //! let reqwest_client = reqwest::Client::builder().build().unwrap(); 13 | //! let client = ClientBuilder::new(reqwest_client) 14 | //! // Insert the tracing middleware 15 | //! .with(TracingMiddleware::default()) 16 | //! .build(); 17 | //! 18 | //! let resp = client.get("https://truelayer.com").send().await.unwrap(); 19 | //! # Ok(()) 20 | //! # } 21 | //! ``` 22 | //! 23 | //! To customise the span names use [`OtelName`]. 24 | //! ```no_run 25 | //! # use reqwest_middleware::Result; 26 | //! use reqwest_middleware::{ClientBuilder, Extension}; 27 | //! use reqwest_tracing::{ 28 | //! TracingMiddleware, OtelName 29 | //! }; 30 | //! # async fn example() -> Result<()> { 31 | //! let reqwest_client = reqwest::Client::builder().build().unwrap(); 32 | //! let client = ClientBuilder::new(reqwest_client) 33 | //! // Inserts the extension before the request is started 34 | //! .with_init(Extension(OtelName("my-client".into()))) 35 | //! // Makes use of that extension to specify the otel name 36 | //! .with(TracingMiddleware::default()) 37 | //! .build(); 38 | //! 39 | //! let resp = client.get("https://truelayer.com").send().await.unwrap(); 40 | //! 41 | //! // Or specify it on the individual request (will take priority) 42 | //! let resp = client.post("https://api.truelayer.com/payment") 43 | //! .with_extension(OtelName("POST /payment".into())) 44 | //! .send() 45 | //! .await 46 | //! .unwrap(); 47 | //! # Ok(()) 48 | //! # } 49 | //! ``` 50 | //! 51 | //! In this example we define a custom span builder to calculate the request time elapsed and we register the [`TracingMiddleware`]. 52 | //! 53 | //! Note that Opentelemetry tracks start and stop already, there is no need to have a custom builder like this. 54 | //! ```rust 55 | //! use reqwest_middleware::Result; 56 | //! use http::Extensions; 57 | //! use reqwest::{Request, Response}; 58 | //! use reqwest_middleware::ClientBuilder; 59 | //! use reqwest_tracing::{ 60 | //! default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend, TracingMiddleware 61 | //! }; 62 | //! use tracing::Span; 63 | //! use std::time::{Duration, Instant}; 64 | //! 65 | //! pub struct TimeTrace; 66 | //! 67 | //! impl ReqwestOtelSpanBackend for TimeTrace { 68 | //! fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { 69 | //! extension.insert(Instant::now()); 70 | //! reqwest_otel_span!(name="example-request", req, time_elapsed = tracing::field::Empty) 71 | //! } 72 | //! 73 | //! fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { 74 | //! let time_elapsed = extension.get::().unwrap().elapsed().as_millis() as i64; 75 | //! default_on_request_end(span, outcome); 76 | //! span.record("time_elapsed", &time_elapsed); 77 | //! } 78 | //! } 79 | //! 80 | //! let http = ClientBuilder::new(reqwest::Client::new()) 81 | //! .with(TracingMiddleware::::new()) 82 | //! .build(); 83 | //! ``` 84 | 85 | mod middleware; 86 | #[cfg(any( 87 | feature = "opentelemetry_0_20", 88 | feature = "opentelemetry_0_21", 89 | feature = "opentelemetry_0_22", 90 | feature = "opentelemetry_0_23", 91 | feature = "opentelemetry_0_24", 92 | feature = "opentelemetry_0_25", 93 | feature = "opentelemetry_0_26", 94 | feature = "opentelemetry_0_27", 95 | feature = "opentelemetry_0_28", 96 | feature = "opentelemetry_0_29", 97 | feature = "opentelemetry_0_30", 98 | ))] 99 | mod otel; 100 | mod reqwest_otel_span_builder; 101 | pub use middleware::TracingMiddleware; 102 | pub use reqwest_otel_span_builder::{ 103 | default_on_request_end, default_on_request_failure, default_on_request_success, 104 | default_span_name, DefaultSpanBackend, DisableOtelPropagation, OtelName, OtelPathNames, 105 | ReqwestOtelSpanBackend, SpanBackendWithUrl, ERROR_CAUSE_CHAIN, ERROR_MESSAGE, 106 | HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, OTEL_KIND, OTEL_NAME, OTEL_STATUS_CODE, 107 | SERVER_ADDRESS, SERVER_PORT, URL_FULL, URL_SCHEME, USER_AGENT_ORIGINAL, 108 | }; 109 | 110 | #[cfg(feature = "deprecated_attributes")] 111 | pub use reqwest_otel_span_builder::{ 112 | HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL, HTTP_USER_AGENT, NET_HOST_PORT, 113 | }; 114 | 115 | #[doc(hidden)] 116 | pub mod reqwest_otel_span_macro; 117 | -------------------------------------------------------------------------------- /reqwest-tracing/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.5.8] - 2025-06-16 11 | 12 | ### Added 13 | - Added support for OpenTelemetry `0.30` ([#236](https://github.com/TrueLayer/reqwest-middleware/pull/236)) 14 | 15 | ## [0.5.7] - 2025-04-08 16 | 17 | ### Added 18 | - Added support for OpenTelemetry `0.29` ([#228](https://github.com/TrueLayer/reqwest-middleware/pull/228)) 19 | 20 | ## [0.5.6] - 2025-02-24 21 | 22 | ### Added 23 | - Added support for OpenTelemetry `0.28` ([#215](https://github.com/TrueLayer/reqwest-middleware/pull/215)) 24 | 25 | ## [0.5.5] - 2024-12-02 26 | 27 | ### Added 28 | - Added support for OpenTelemetry `0.27` ([#201](https://github.com/TrueLayer/reqwest-middleware/pull/201)) 29 | 30 | ## [0.5.4] - 2024-11-08 31 | 32 | ### Added 33 | - Added support for OpenTelemetry `0.25` ([#188](https://github.com/TrueLayer/reqwest-middleware/pull/188)) 34 | - Added support for OpenTelemetry `0.26` ([#188](https://github.com/TrueLayer/reqwest-middleware/pull/188)) 35 | - Added support reqwest-middleware `0.4` next to `0.3` 36 | 37 | ### Changed 38 | - Restore adding `http.url` attribute when using `SpanBackendWithUrl` middleware with the `deprecated_attributes` feature enabled 39 | 40 | ## [0.5.3] - 2024-07-15 41 | 42 | ### Added 43 | - Added support for OpenTelemetry `0.24` ([#171](https://github.com/TrueLayer/reqwest-middleware/pull/171)) 44 | 45 | ### Fixed 46 | - Fixed, `deprecated_attributes` feature, failing to compile ([#172](https://github.com/TrueLayer/reqwest-middleware/pull/172)) 47 | 48 | ## [0.5.2] - 2024-07-15 49 | 50 | ### Added 51 | - Added feature flag, `deprecated_attributes`, for emitting [deprecated opentelemetry HTTP attributes](https://opentelemetry.io/docs/specs/semconv/http/migration-guide/) alongside the stable ones used by default 52 | 53 | ## [0.5.1] - 2024-06-28 54 | 55 | ### Added 56 | - Added support for `opentelemetry` version `0.23`. 57 | 58 | ## [0.5.0] - 2024-04-10 59 | 60 | ### Breaking changes 61 | - Upgraded `reqwest-middleware` to `0.3.0`. 62 | - Removed support for `opentelemetry` 0.13 to 0.19 63 | - The keys emitted by the crate now match the stable Semantic Conventions for HTTP Spans. 64 | 65 | ### Changed 66 | - The keys emitted by the crate now match the stable Semantic Conventions for HTTP Spans. 67 | - Opentelemetry features are now additive. 68 | 69 | ## [0.4.8] - 2024-03-11 70 | 71 | ### Added 72 | - Add support for opentelemetry 0.22 73 | 74 | ## [0.4.6] - 2023-08-23 75 | 76 | ### Added 77 | - Add support for opentelemetry 0.20 78 | 79 | ## [0.4.5] - 2023-06-20 80 | 81 | ### Added 82 | - A new extension `DisableOtelPropagation` which stops opentelemetry contexts propagating 83 | - Support for opentelemetry 0.19 84 | 85 | ## [0.4.4] - 2023-05-15 86 | 87 | ### Added 88 | - A new `default_span_name` method for use in custom span backends. 89 | 90 | ## [0.4.3] - 2023-05-15 91 | 92 | ### Fixed 93 | - Fix span and http status codes 94 | 95 | ## [0.4.2] - 2023-05-12 96 | 97 | ### Added 98 | - `OtelPathNames` extension to provide known parameterized paths that will be used in span names 99 | 100 | ### Changed 101 | - `DefaultSpanBackend` and `SpanBackendWithUrl` default span name to HTTP method name instead of `reqwest-http-client` 102 | 103 | ## [0.4.1] - 2023-03-09 104 | 105 | ### Added 106 | 107 | - Support for `wasm32-unknown-unknown` target 108 | 109 | ## [0.4.0] - 2022-11-15 110 | 111 | ### Changed 112 | - Updated `reqwest-middleware` to `0.2.0` 113 | - Before, `root_span!`/`DefaultSpanBacked` would name your spans `{METHOD} {PATH}`. Since this can be quite 114 | high cardinality, this was changed and now the macro requires an explicit otel name. 115 | `DefaultSpanBacked`/`SpanBackendWithUrl` will default to `reqwest-http-client` but this can be configured 116 | using the `OtelName` Request Initialiser. 117 | 118 | ### Added 119 | - `SpanBackendWithUrl` for capturing `http.url` in traces 120 | - `OtelName` Request Initialiser Extension for configuring 121 | 122 | ## [0.3.1] - 2022-09-21 123 | - Added support for `opentelemetry` version `0.18`. 124 | 125 | ## [0.3.0] - 2022-06-10 126 | ### Breaking 127 | - Created `ReqwestOtelSpanBackend` trait with `reqwest_otel_span` macro to provide extendable default request otel fields 128 | 129 | ## [0.2.3] - 2022-06-23 130 | ### Fixed 131 | - Fix how we set the OpenTelemetry span status, based on the HTTP response status. 132 | 133 | ## [0.2.2] - 2022-04-21 134 | ### Fixed 135 | - Opentelemetry context is now propagated when the request span is disabled. 136 | 137 | ## [0.2.1] - 2022-02-21 138 | ### Changed 139 | - Updated `reqwest-middleware` to `0.1.5` 140 | 141 | ## [0.2.0] - 2021-11-30 142 | ### Breaking 143 | - Update to `tracing-subscriber` `0.3.x` when `opentelemetry_0_16` is active. 144 | 145 | ## [0.1.3] - 2021-09-28 146 | ### Changed 147 | - Disabled default features on `reqwest` 148 | - Replaced `truelayer-extensions` with `task-local-extensions` 149 | - Updated `reqwest-middleware` to `0.1.2` 150 | 151 | ## [0.1.2] - 2021-09-15 152 | ### Changed 153 | - Updated `reqwest-middleware` dependency to `0.1.1`. 154 | 155 | ## [0.1.1] - 2021-08-30 156 | ### Added 157 | - Support for opentelemtry `0.15` and `0.16`. 158 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | name: CI # Continuous Integration 6 | 7 | jobs: 8 | 9 | test: 10 | name: Run test suite 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | - name: Install Rust 16 | uses: dtolnay/rust-toolchain@stable 17 | - uses: actions-rs/cargo@v1 18 | with: 19 | command: test 20 | args: --workspace --all-targets 21 | 22 | test-features-reqwest-middleware: 23 | name: Run test suite for `reqwest-middleware` with every feature combination 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | - name: Install Rust 29 | uses: dtolnay/rust-toolchain@stable 30 | - name: Install cargo-hack 31 | uses: taiki-e/install-action@cargo-hack 32 | - name: test 33 | run: cargo hack test --feature-powerset -p reqwest-middleware 34 | check-wasm32-reqwest-middleware: 35 | name: Run `cargo check` for `reqwest-middleware` on `wasm32-unknown-unknown` 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | - name: Install Rust 41 | uses: dtolnay/rust-toolchain@stable 42 | with: 43 | targets: wasm32-unknown-unknown 44 | - uses: actions-rs/cargo@v1 45 | with: 46 | command: build 47 | args: --target wasm32-unknown-unknown -p reqwest-middleware --all-features 48 | test-features-retry: 49 | name: Run test suite for `reqwest-retry` with every feature combination 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v4 54 | - name: Install Rust 55 | uses: dtolnay/rust-toolchain@stable 56 | - name: Install cargo-hack 57 | uses: taiki-e/install-action@cargo-hack 58 | - name: test 59 | run: cargo hack test --feature-powerset -p reqwest-retry 60 | test-features-reqwest-tracing: 61 | name: Run test suite for `reqwest-tracing` with every feature combination 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Checkout repository 65 | uses: actions/checkout@v4 66 | - name: Install Rust 67 | uses: dtolnay/rust-toolchain@stable 68 | - name: Install cargo-hack 69 | uses: taiki-e/install-action@cargo-hack 70 | # Old opentelemetry features group to improve performance 71 | - name: test 72 | run: cargo hack test --feature-powerset -p reqwest-tracing --exclude-features opentelemetry_0_20 --group-features opentelemetry_0_21,opentelemetry_0_22,opentelemetry_0_23,opentelemetry_0_24,opentelemetry_0_25,opentelemetry_0_26,opentelemetry_0_27 73 | 74 | rustfmt: 75 | name: Rustfmt 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Checkout repository 79 | uses: actions/checkout@v4 80 | - name: Install Rust 81 | uses: dtolnay/rust-toolchain@stable 82 | - name: Check formatting 83 | uses: actions-rs/cargo@v1 84 | with: 85 | command: fmt 86 | args: --all -- --check 87 | 88 | clippy: 89 | name: Run clippy 90 | runs-on: ubuntu-latest 91 | steps: 92 | - name: Checkout repository 93 | uses: actions/checkout@v4 94 | - name: Install Rust 95 | uses: dtolnay/rust-toolchain@stable 96 | - name: Clippy check 97 | uses: actions-rs/cargo@v1 98 | with: 99 | command: clippy 100 | args: --all-targets --all-features --workspace -- -D warnings 101 | 102 | 103 | clippy-features: 104 | name: Run clippy for every feature combination 105 | runs-on: ubuntu-latest 106 | steps: 107 | - name: Checkout repository 108 | uses: actions/checkout@v4 109 | - name: Install Rust 110 | uses: dtolnay/rust-toolchain@stable 111 | - name: Install cargo-hack 112 | uses: taiki-e/install-action@cargo-hack 113 | - name: test 114 | run: cargo hack clippy --feature-powerset 115 | 116 | typos: 117 | name: Spell Check with Typos 118 | runs-on: ubuntu-latest 119 | steps: 120 | - name: Checkout Actions Repository 121 | uses: actions/checkout@v4 122 | - name: Check spelling 123 | uses: crate-ci/typos@master 124 | 125 | docs: 126 | name: Docs 127 | runs-on: ubuntu-latest 128 | steps: 129 | - name: Checkout repository 130 | uses: actions/checkout@v4 131 | - name: Install Rust 132 | uses: dtolnay/rust-toolchain@stable 133 | - name: Check documentation 134 | env: 135 | RUSTDOCFLAGS: -D warnings 136 | uses: actions-rs/cargo@v1 137 | with: 138 | command: doc 139 | args: --no-deps --document-private-items --all-features --workspace 140 | 141 | publish-check: 142 | name: Publish dry run 143 | runs-on: ubuntu-latest 144 | steps: 145 | - name: Checkout repository 146 | uses: actions/checkout@v4 147 | - name: Install Rust 148 | uses: dtolnay/rust-toolchain@stable 149 | - uses: actions-rs/cargo@v1 150 | with: 151 | command: publish 152 | args: --dry-run --manifest-path reqwest-middleware/Cargo.toml 153 | - uses: actions-rs/cargo@v1 154 | with: 155 | command: publish 156 | args: --dry-run --manifest-path reqwest-retry/Cargo.toml 157 | - uses: actions-rs/cargo@v1 158 | with: 159 | command: publish 160 | args: --dry-run --manifest-path reqwest-tracing/Cargo.toml 161 | -------------------------------------------------------------------------------- /reqwest-tracing/README.md: -------------------------------------------------------------------------------- 1 | # reqwest-tracing 2 | 3 | Opentracing middleware implementation for 4 | [`reqwest-middleware`](https://crates.io/crates/reqwest-middleware). 5 | 6 | [![Crates.io](https://img.shields.io/crates/v/reqwest-tracing.svg)](https://crates.io/crates/reqwest-tracing) 7 | [![Docs.rs](https://docs.rs/reqwest-tracing/badge.svg)](https://docs.rs/reqwest-tracing) 8 | [![CI](https://github.com/TrueLayer/reqwest-middleware/workflows/CI/badge.svg)](https://github.com/TrueLayer/reqwest-middleware/actions) 9 | [![Coverage Status](https://coveralls.io/repos/github/TrueLayer/reqwest-middleware/badge.svg?branch=main&t=UWgSpm)](https://coveralls.io/github/TrueLayer/reqwest-middleware?branch=main) 10 | 11 | ## Overview 12 | 13 | Attach `TracingMiddleware` to your client to automatically trace HTTP requests: 14 | 15 | ```toml 16 | # Cargo.toml 17 | # ... 18 | [dependencies] 19 | opentelemetry = "0.22" 20 | reqwest = { version = "0.12", features = ["rustls-tls"] } 21 | reqwest-middleware = "0.3" 22 | reqwest-retry = "0.5" 23 | reqwest-tracing = { version = "0.5", features = ["opentelemetry_0_22"] } 24 | tokio = { version = "1.12.0", features = ["macros", "rt-multi-thread"] } 25 | tracing = "0.1" 26 | tracing-opentelemetry = "0.23" 27 | tracing-subscriber = "0.3" 28 | http = "1" 29 | ``` 30 | 31 | ```rust,skip 32 | use reqwest_tracing::{default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend, TracingMiddleware}; 33 | use reqwest::{Request, Response}; 34 | use reqwest_middleware::{ClientBuilder, Result}; 35 | use std::time::Instant; 36 | use http::Extensions; 37 | use tracing::Span; 38 | use tracing_subscriber::FmtSubscriber; 39 | use tracing::Level; 40 | 41 | pub struct TimeTrace; 42 | 43 | impl ReqwestOtelSpanBackend for TimeTrace { 44 | fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { 45 | extension.insert(Instant::now()); 46 | reqwest_otel_span!(name="example-request", req, time_elapsed = tracing::field::Empty) 47 | } 48 | 49 | fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { 50 | let time_elapsed = extension.get::().unwrap().elapsed().as_millis() as i64; 51 | default_on_request_end(span, outcome); 52 | span.record("time_elapsed", &time_elapsed); 53 | } 54 | } 55 | 56 | #[tokio::main] 57 | async fn main() { 58 | let subscriber = FmtSubscriber::builder() 59 | .with_max_level(Level::TRACE) 60 | .finish(); 61 | 62 | tracing::subscriber::set_global_default(subscriber) 63 | .expect("setting default subscriber failed"); 64 | 65 | run().await; 66 | } 67 | 68 | async fn run() { 69 | let client = ClientBuilder::new(reqwest::Client::new()) 70 | .with(TracingMiddleware::::new()) 71 | .build(); 72 | 73 | client.get("https://truelayer.com").send().await.unwrap(); 74 | } 75 | ``` 76 | 77 | ```terminal 78 | $ cargo run 79 | 2024-09-10T13:19:52.520194Z TRACE HTTP request{http.request.method=GET url.scheme=https server.address=truelayer.com server.port=443 user_agent.original= otel.kind="client" otel.name=example-request}: hyper_util::client::legacy::pool: checkout waiting for idle connection: ("https", truelayer.com) 80 | 2024-09-10T13:19:52.520303Z TRACE HTTP request{http.request.method=GET url.scheme=https server.address=truelayer.com server.port=443 user_agent.original= otel.kind="client" otel.name=example-request}: hyper_util::client::legacy::connect::http: Http::connect; scheme=Some("https"), host=Some("truelayer.com"), port=None 81 | 2024-09-10T13:19:52.520686Z DEBUG HTTP request{http.request.method=GET url.scheme=https server.address=truelayer.com server.port=443 user_agent.original= otel.kind="client" otel.name=example-request}:resolve{host=truelayer.com}: hyper_util::client::legacy::connect::dns: resolving host="truelayer.com" 82 | 2024-09-10T13:19:52.521847Z DEBUG HTTP request{http.request.method=GET url.scheme=https server.address=truelayer.com server.port=443 user_agent.original= otel.kind="client" otel.name=example-request}: hyper_util::client::legacy::connect::http: connecting to 104.18.24.12:443 83 | 2024-09-10T13:19:52.532045Z DEBUG HTTP request{http.request.method=GET url.scheme=https server.address=truelayer.com server.port=443 user_agent.original= otel.kind="client" otel.name=example-request}: hyper_util::client::legacy::connect::http: connected to 104.18.24.12:443 84 | 2024-09-10T13:19:52.548050Z TRACE HTTP request{http.request.method=GET url.scheme=https server.address=truelayer.com server.port=443 user_agent.original= otel.kind="client" otel.name=example-request}: hyper_util::client::legacy::client: http1 handshake complete, spawning background dispatcher task 85 | 2024-09-10T13:19:52.548651Z TRACE HTTP request{http.request.method=GET url.scheme=https server.address=truelayer.com server.port=443 user_agent.original= otel.kind="client" otel.name=example-request}: hyper_util::client::legacy::pool: checkout dropped for ("https", truelayer.com) 86 | ``` 87 | 88 | See the [`tracing`](https://crates.io/crates/tracing) crate for more information on how to set up a 89 | tracing subscriber to make use of the spans. 90 | 91 | ## How to install 92 | 93 | Add `reqwest-tracing` to your dependencies. Optionally enable opentelemetry integration by enabling 94 | an opentelemetry version feature: 95 | 96 | ```toml 97 | [dependencies] 98 | # ... 99 | reqwest-tracing = { version = "0.5.0", features = ["opentelemetry_0_22"] } 100 | ``` 101 | 102 | Available opentelemetry features are `opentelemetry_0_22`, `opentelemetry_0_21`, and `opentelemetry_0_20`, 103 | 104 | #### License 105 | 106 | 107 | Licensed under either of Apache License, Version 108 | 2.0 or MIT license at your option. 109 | 110 | 111 |
112 | 113 | 114 | Unless you explicitly state otherwise, any contribution intentionally submitted 115 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 116 | dual licensed as above, without any additional terms or conditions. 117 | 118 | -------------------------------------------------------------------------------- /reqwest-retry/tests/all/helpers/simple_server.rs: -------------------------------------------------------------------------------- 1 | use futures::future::BoxFuture; 2 | use std::error::Error; 3 | use std::fmt; 4 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 5 | use tokio::net::{TcpListener, TcpStream}; 6 | 7 | type CustomMessageHandler = Box< 8 | dyn Fn(TcpStream) -> BoxFuture<'static, Result<(), Box>> + Send + Sync, 9 | >; 10 | 11 | /// This is a simple server that returns the responses given at creation time: [`self.raw_http_responses`] following a round-robin mechanism. 12 | pub struct SimpleServer { 13 | listener: TcpListener, 14 | port: u16, 15 | host: String, 16 | raw_http_responses: Vec, 17 | calls_counter: usize, 18 | custom_handler: Option, 19 | } 20 | 21 | /// Request-Line = Method SP Request-URI SP HTTP-Version CRLF 22 | struct Request<'a> { 23 | method: &'a str, 24 | uri: &'a str, 25 | http_version: &'a str, 26 | } 27 | 28 | impl fmt::Display for Request<'_> { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | write!(f, "{} {} {}\r\n", self.method, self.uri, self.http_version) 31 | } 32 | } 33 | 34 | impl SimpleServer { 35 | /// Creates an instance of a [`SimpleServer`] 36 | /// If [`port`] is None os Some(0), it gets randomly chosen between the available ones. 37 | pub async fn new( 38 | host: &str, 39 | port: Option, 40 | raw_http_responses: Vec, 41 | ) -> Result { 42 | let port = port.unwrap_or(0); 43 | let listener = TcpListener::bind(format!("{}:{}", host, port)).await?; 44 | 45 | let port = listener.local_addr()?.port(); 46 | 47 | Ok(Self { 48 | listener, 49 | port, 50 | host: host.to_string(), 51 | raw_http_responses, 52 | calls_counter: 0, 53 | custom_handler: None, 54 | }) 55 | } 56 | 57 | pub fn set_custom_handler( 58 | &mut self, 59 | custom_handler: impl Fn(TcpStream) -> BoxFuture<'static, Result<(), Box>> 60 | + Send 61 | + Sync 62 | + 'static, 63 | ) -> &mut Self { 64 | self.custom_handler.replace(Box::new(custom_handler)); 65 | self 66 | } 67 | 68 | /// Returns the uri in which the server is listening to. 69 | pub fn uri(&self) -> String { 70 | format!("http://{}:{}", self.host, self.port) 71 | } 72 | 73 | /// Starts the TcpListener and handles the requests. 74 | pub async fn start(mut self) { 75 | loop { 76 | match self.listener.accept().await { 77 | Ok((stream, _)) => { 78 | match self.handle_connection(stream).await { 79 | Ok(_) => (), 80 | Err(e) => { 81 | println!("Error handling connection: {}", e); 82 | } 83 | } 84 | self.calls_counter += 1; 85 | } 86 | Err(e) => { 87 | println!("Connection failed: {}", e); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /// Asyncrounously reads from the buffer and handle the request. 94 | /// It first checks that the format is correct, then returns the response. 95 | /// 96 | /// Returns a 400 if the request if formatted badly. 97 | async fn handle_connection(&self, mut stream: TcpStream) -> Result<(), Box> { 98 | if let Some(ref custom_handler) = self.custom_handler { 99 | return custom_handler(stream).await; 100 | } 101 | 102 | let mut buffer = vec![0; 1024]; 103 | 104 | let n = stream.read(&mut buffer).await.unwrap(); 105 | 106 | let request = String::from_utf8_lossy(&buffer[..n]); 107 | let request_line = request.lines().next().unwrap(); 108 | 109 | let response = match Self::parse_request_line(request_line) { 110 | Ok(request) => { 111 | println!("== Request == \n{}\n=============", request); 112 | self.get_response().clone() 113 | } 114 | Err(e) => { 115 | println!("++ Bad request: {} ++++++", e); 116 | self.get_bad_request_response() 117 | } 118 | }; 119 | 120 | println!("-- Response --\n{}\n--------------", response.clone()); 121 | stream.write_all(response.as_bytes()).await.unwrap(); 122 | stream.flush().await.unwrap(); 123 | 124 | Ok(()) 125 | } 126 | 127 | /// Parses the request line and checks that it contains the method, uri and http_version parts. 128 | /// It does not check if the content of the checked parts is correct. It just checks the format (it contains enough parts) of the request. 129 | fn parse_request_line(request: &str) -> Result, Box> { 130 | let mut parts = request.split_whitespace(); 131 | 132 | let method = parts.next().ok_or("Method not specified")?; 133 | 134 | let uri = parts.next().ok_or("URI not specified")?; 135 | 136 | let http_version = parts.next().ok_or("HTTP version not specified")?; 137 | 138 | Ok(Request { 139 | method, 140 | uri, 141 | http_version, 142 | }) 143 | } 144 | 145 | /// Returns the response to use based on the calls counter. 146 | /// It uses a round-robin mechanism. 147 | fn get_response(&self) -> String { 148 | let index = if self.calls_counter >= self.raw_http_responses.len() { 149 | self.raw_http_responses.len() % self.calls_counter 150 | } else { 151 | self.calls_counter 152 | }; 153 | self.raw_http_responses[index].clone() 154 | } 155 | 156 | /// Returns the raw HTTP response in case of a 400 Bad Request. 157 | fn get_bad_request_response(&self) -> String { 158 | "HTTP/1.1 400 Bad Request\r\n\r\n".to_string() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /reqwest-tracing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reqwest-tracing" 3 | version = "0.5.8" 4 | authors = ["Rodrigo Gryzinski "] 5 | edition = "2018" 6 | description = "Opentracing middleware for reqwest." 7 | repository = "https://github.com/TrueLayer/reqwest-middleware" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["reqwest", "http", "middleware", "opentelemetry", "tracing"] 10 | categories = ["web-programming::http-client"] 11 | 12 | [features] 13 | opentelemetry_0_20 = ["opentelemetry_0_20_pkg", "tracing-opentelemetry_0_21_pkg"] 14 | opentelemetry_0_21 = ["opentelemetry_0_21_pkg", "tracing-opentelemetry_0_22_pkg"] 15 | opentelemetry_0_22 = ["opentelemetry_0_22_pkg", "tracing-opentelemetry_0_23_pkg"] 16 | opentelemetry_0_23 = ["opentelemetry_0_23_pkg", "tracing-opentelemetry_0_24_pkg"] 17 | opentelemetry_0_24 = ["opentelemetry_0_24_pkg", "tracing-opentelemetry_0_25_pkg"] 18 | opentelemetry_0_25 = ["opentelemetry_0_25_pkg", "tracing-opentelemetry_0_26_pkg"] 19 | opentelemetry_0_26 = ["opentelemetry_0_26_pkg", "tracing-opentelemetry_0_27_pkg"] 20 | opentelemetry_0_27 = ["opentelemetry_0_27_pkg", "tracing-opentelemetry_0_28_pkg"] 21 | opentelemetry_0_28 = ["opentelemetry_0_28_pkg", "tracing-opentelemetry_0_29_pkg"] 22 | opentelemetry_0_29 = ["opentelemetry_0_29_pkg", "tracing-opentelemetry_0_30_pkg"] 23 | opentelemetry_0_30 = ["opentelemetry_0_30_pkg", "tracing-opentelemetry_0_31_pkg"] 24 | # This feature ensures that both the old (deprecated) and new attributes are published simultaneously. 25 | # By doing so, we maintain backward compatibility, allowing existing code that relies on the old attributes 26 | # to continue functioning while encouraging the transition to the new attributes. 27 | deprecated_attributes = [] 28 | 29 | [dependencies] 30 | reqwest-middleware = { version = ">0.3.0, <0.5.0", path = "../reqwest-middleware" } 31 | 32 | anyhow = "1.0.70" 33 | async-trait = "0.1.51" 34 | matchit = "0.8.0" 35 | http = "1" 36 | reqwest = { version = "0.12.0", default-features = false } 37 | tracing = "0.1.26" 38 | 39 | opentelemetry_0_20_pkg = { package = "opentelemetry", version = "0.20.0", optional = true } 40 | opentelemetry_0_21_pkg = { package = "opentelemetry", version = "0.21.0", optional = true } 41 | opentelemetry_0_22_pkg = { package = "opentelemetry", version = "0.22.0", optional = true } 42 | opentelemetry_0_23_pkg = { package = "opentelemetry", version = "0.23.0", optional = true } 43 | opentelemetry_0_24_pkg = { package = "opentelemetry", version = "0.24.0", optional = true } 44 | opentelemetry_0_25_pkg = { package = "opentelemetry", version = "0.25.0", optional = true } 45 | opentelemetry_0_26_pkg = { package = "opentelemetry", version = "0.26.0", optional = true } 46 | opentelemetry_0_27_pkg = { package = "opentelemetry", version = "0.27.0", optional = true } 47 | opentelemetry_0_28_pkg = { package = "opentelemetry", version = "0.28.0", optional = true } 48 | opentelemetry_0_29_pkg = { package = "opentelemetry", version = "0.29.0", optional = true } 49 | opentelemetry_0_30_pkg = { package = "opentelemetry", version = "0.30.0", optional = true } 50 | tracing-opentelemetry_0_21_pkg = { package = "tracing-opentelemetry", version = "0.21.0", optional = true } 51 | tracing-opentelemetry_0_22_pkg = { package = "tracing-opentelemetry", version = "0.22.0", optional = true } 52 | tracing-opentelemetry_0_23_pkg = { package = "tracing-opentelemetry", version = "0.23.0", optional = true } 53 | tracing-opentelemetry_0_24_pkg = { package = "tracing-opentelemetry", version = "0.24.0", optional = true } 54 | tracing-opentelemetry_0_25_pkg = { package = "tracing-opentelemetry", version = "0.25.0", optional = true } 55 | tracing-opentelemetry_0_26_pkg = { package = "tracing-opentelemetry", version = "0.26.0", optional = true } 56 | tracing-opentelemetry_0_27_pkg = { package = "tracing-opentelemetry", version = "0.27.0", optional = true } 57 | tracing-opentelemetry_0_28_pkg = { package = "tracing-opentelemetry", version = "0.28.0", optional = true } 58 | tracing-opentelemetry_0_29_pkg = { package = "tracing-opentelemetry", version = "0.29.0", optional = true } 59 | tracing-opentelemetry_0_30_pkg = { package = "tracing-opentelemetry", version = "0.30.0", optional = true } 60 | tracing-opentelemetry_0_31_pkg = { package = "tracing-opentelemetry", version = "0.31.0", optional = true } 61 | 62 | [target.'cfg(target_arch = "wasm32")'.dependencies] 63 | getrandom = { version = "0.2.0", features = ["js"] } 64 | 65 | [dev-dependencies] 66 | tokio = { version = "1.0.0", features = ["macros"] } 67 | tracing_subscriber = { package = "tracing-subscriber", version = "0.3.0" } 68 | wiremock = "0.6.0" 69 | reqwest = { version = "0.12.0", features = ["rustls-tls"] } 70 | 71 | opentelemetry_sdk_0_21 = { package = "opentelemetry_sdk", version = "0.21.0", features = ["trace"] } 72 | opentelemetry_sdk_0_22 = { package = "opentelemetry_sdk", version = "0.22.0", features = ["trace"] } 73 | opentelemetry_sdk_0_23 = { package = "opentelemetry_sdk", version = "0.23.0", features = ["trace"] } 74 | opentelemetry_sdk_0_24 = { package = "opentelemetry_sdk", version = "0.24.1", features = ["trace"] } 75 | opentelemetry_sdk_0_25 = { package = "opentelemetry_sdk", version = "0.25.0", features = ["trace"] } 76 | opentelemetry_sdk_0_26 = { package = "opentelemetry_sdk", version = "0.26.0", features = ["trace"] } 77 | opentelemetry_sdk_0_27 = { package = "opentelemetry_sdk", version = "0.27.0", features = ["trace"] } 78 | opentelemetry_sdk_0_28 = { package = "opentelemetry_sdk", version = "0.28.0", features = ["trace"] } 79 | opentelemetry_sdk_0_29 = { package = "opentelemetry_sdk", version = "0.29.0", features = ["trace"] } 80 | opentelemetry_sdk_0_30 = { package = "opentelemetry_sdk", version = "0.30.0", features = ["trace"] } 81 | opentelemetry_stdout_0_1 = { package = "opentelemetry-stdout", version = "0.1.0", features = ["trace"] } 82 | opentelemetry_stdout_0_2 = { package = "opentelemetry-stdout", version = "0.2.0", features = ["trace"] } 83 | opentelemetry_stdout_0_3 = { package = "opentelemetry-stdout", version = "0.3.0", features = ["trace"] } 84 | opentelemetry_stdout_0_4 = { package = "opentelemetry-stdout", version = "0.4.0", features = ["trace"] } 85 | opentelemetry_stdout_0_5 = { package = "opentelemetry-stdout", version = "0.5.0", features = ["trace"] } 86 | -------------------------------------------------------------------------------- /reqwest-retry/src/retryable_strategy.rs: -------------------------------------------------------------------------------- 1 | use crate::retryable::Retryable; 2 | use http::StatusCode; 3 | use reqwest_middleware::Error; 4 | 5 | /// A strategy to create a [`Retryable`] from a [`Result`] 6 | /// 7 | /// A [`RetryableStrategy`] has a single `handler` functions. 8 | /// The result of calling the request could be: 9 | /// - [`reqwest::Response`] In case the request has been sent and received correctly 10 | /// This could however still mean that the server responded with a erroneous response. 11 | /// For example a HTTP statuscode of 500 12 | /// - [`reqwest_middleware::Error`] In this case the request actually failed. 13 | /// This could, for example, be caused by a timeout on the connection. 14 | /// 15 | /// Example: 16 | /// 17 | /// ``` 18 | /// use reqwest_retry::{default_on_request_failure, policies::ExponentialBackoff, Retryable, RetryableStrategy, RetryTransientMiddleware}; 19 | /// use reqwest::{Request, Response}; 20 | /// use reqwest_middleware::{ClientBuilder, Middleware, Next, Result}; 21 | /// use http::Extensions; 22 | /// 23 | /// // Log each request to show that the requests will be retried 24 | /// struct LoggingMiddleware; 25 | /// 26 | /// #[async_trait::async_trait] 27 | /// impl Middleware for LoggingMiddleware { 28 | /// async fn handle( 29 | /// &self, 30 | /// req: Request, 31 | /// extensions: &mut Extensions, 32 | /// next: Next<'_>, 33 | /// ) -> Result { 34 | /// println!("Request started {}", req.url()); 35 | /// let res = next.run(req, extensions).await; 36 | /// println!("Request finished"); 37 | /// res 38 | /// } 39 | /// } 40 | /// 41 | /// // Just a toy example, retry when the successful response code is 201, else do nothing. 42 | /// struct Retry201; 43 | /// impl RetryableStrategy for Retry201 { 44 | /// fn handle(&self, res: &Result) -> Option { 45 | /// match res { 46 | /// // retry if 201 47 | /// Ok(success) if success.status() == 201 => Some(Retryable::Transient), 48 | /// // otherwise do not retry a successful request 49 | /// Ok(success) => None, 50 | /// // but maybe retry a request failure 51 | /// Err(error) => default_on_request_failure(error), 52 | /// } 53 | /// } 54 | /// } 55 | /// 56 | /// #[tokio::main] 57 | /// async fn main() { 58 | /// // Exponential backoff with max 2 retries 59 | /// let retry_policy = ExponentialBackoff::builder() 60 | /// .build_with_max_retries(2); 61 | /// 62 | /// // Create the actual middleware, with the exponential backoff and custom retry strategy. 63 | /// let ret_s = RetryTransientMiddleware::new_with_policy_and_strategy( 64 | /// retry_policy, 65 | /// Retry201, 66 | /// ); 67 | /// 68 | /// let client = ClientBuilder::new(reqwest::Client::new()) 69 | /// // Retry failed requests. 70 | /// .with(ret_s) 71 | /// // Log the requests 72 | /// .with(LoggingMiddleware) 73 | /// .build(); 74 | /// 75 | /// // Send request which should get a 201 response. So it will be retried 76 | /// let r = client 77 | /// .get("https://httpbin.org/status/201") 78 | /// .send() 79 | /// .await; 80 | /// println!("{:?}", r); 81 | /// 82 | /// // Send request which should get a 200 response. So it will not be retried 83 | /// let r = client 84 | /// .get("https://httpbin.org/status/200") 85 | /// .send() 86 | /// .await; 87 | /// println!("{:?}", r); 88 | /// } 89 | /// ``` 90 | pub trait RetryableStrategy { 91 | fn handle(&self, res: &Result) -> Option; 92 | } 93 | 94 | /// The default [`RetryableStrategy`] for [`RetryTransientMiddleware`](crate::RetryTransientMiddleware). 95 | pub struct DefaultRetryableStrategy; 96 | 97 | impl RetryableStrategy for DefaultRetryableStrategy { 98 | fn handle(&self, res: &Result) -> Option { 99 | match res { 100 | Ok(success) => default_on_request_success(success), 101 | Err(error) => default_on_request_failure(error), 102 | } 103 | } 104 | } 105 | 106 | /// Default request success retry strategy. 107 | /// 108 | /// Will only retry if: 109 | /// * The status was 5XX (server error) 110 | /// * The status was 408 (request timeout) or 429 (too many requests) 111 | /// 112 | /// Note that success here means that the request finished without interruption, not that it was logically OK. 113 | pub fn default_on_request_success(success: &reqwest::Response) -> Option { 114 | let status = success.status(); 115 | if status.is_server_error() { 116 | Some(Retryable::Transient) 117 | } else if status.is_client_error() 118 | && status != StatusCode::REQUEST_TIMEOUT 119 | && status != StatusCode::TOO_MANY_REQUESTS 120 | { 121 | Some(Retryable::Fatal) 122 | } else if status.is_success() { 123 | None 124 | } else if status == StatusCode::REQUEST_TIMEOUT || status == StatusCode::TOO_MANY_REQUESTS { 125 | Some(Retryable::Transient) 126 | } else { 127 | Some(Retryable::Fatal) 128 | } 129 | } 130 | 131 | /// Default request failure retry strategy. 132 | /// 133 | /// Will only retry if the request failed due to a network error 134 | pub fn default_on_request_failure(error: &Error) -> Option { 135 | match error { 136 | // If something fails in the middleware we're screwed. 137 | Error::Middleware(_) => Some(Retryable::Fatal), 138 | Error::Reqwest(error) => { 139 | #[cfg(not(target_arch = "wasm32"))] 140 | let is_connect = error.is_connect(); 141 | #[cfg(target_arch = "wasm32")] 142 | let is_connect = false; 143 | if error.is_timeout() || is_connect { 144 | Some(Retryable::Transient) 145 | } else if error.is_body() 146 | || error.is_decode() 147 | || error.is_builder() 148 | || error.is_redirect() 149 | { 150 | Some(Retryable::Fatal) 151 | } else if error.is_request() { 152 | // It seems that hyper::Error(IncompleteMessage) is not correctly handled by reqwest. 153 | // Here we check if the Reqwest error was originated by hyper and map it consistently. 154 | #[cfg(not(target_arch = "wasm32"))] 155 | if let Some(hyper_error) = get_source_error_type::(&error) { 156 | // The hyper::Error(IncompleteMessage) is raised if the HTTP response is well formatted but does not contain all the bytes. 157 | // This can happen when the server has started sending back the response but the connection is cut halfway through. 158 | // We can safely retry the call, hence marking this error as [`Retryable::Transient`]. 159 | // Instead hyper::Error(Canceled) is raised when the connection is 160 | // gracefully closed on the server side. 161 | if hyper_error.is_incomplete_message() || hyper_error.is_canceled() { 162 | Some(Retryable::Transient) 163 | 164 | // Try and downcast the hyper error to io::Error if that is the 165 | // underlying error, and try and classify it. 166 | } else if let Some(io_error) = 167 | get_source_error_type::(hyper_error) 168 | { 169 | Some(classify_io_error(io_error)) 170 | } else { 171 | Some(Retryable::Fatal) 172 | } 173 | } else { 174 | Some(Retryable::Fatal) 175 | } 176 | #[cfg(target_arch = "wasm32")] 177 | Some(Retryable::Fatal) 178 | } else { 179 | // We omit checking if error.is_status() since we check that already. 180 | // However, if Response::error_for_status is used the status will still 181 | // remain in the response object. 182 | None 183 | } 184 | } 185 | } 186 | } 187 | 188 | #[cfg(not(target_arch = "wasm32"))] 189 | fn classify_io_error(error: &std::io::Error) -> Retryable { 190 | match error.kind() { 191 | std::io::ErrorKind::ConnectionReset | std::io::ErrorKind::ConnectionAborted => { 192 | Retryable::Transient 193 | } 194 | _ => Retryable::Fatal, 195 | } 196 | } 197 | 198 | /// Downcasts the given err source into T. 199 | #[cfg(not(target_arch = "wasm32"))] 200 | fn get_source_error_type( 201 | err: &dyn std::error::Error, 202 | ) -> Option<&T> { 203 | let mut source = err.source(); 204 | 205 | while let Some(err) = source { 206 | if let Some(err) = err.downcast_ref::() { 207 | return Some(err); 208 | } 209 | 210 | source = err.source(); 211 | } 212 | None 213 | } 214 | -------------------------------------------------------------------------------- /reqwest-retry/src/middleware.rs: -------------------------------------------------------------------------------- 1 | //! `RetryTransientMiddleware` implements retrying requests on transient errors. 2 | use std::time::{Duration, SystemTime}; 3 | 4 | use crate::retryable_strategy::RetryableStrategy; 5 | use crate::{retryable::Retryable, retryable_strategy::DefaultRetryableStrategy, RetryError}; 6 | use anyhow::anyhow; 7 | use http::Extensions; 8 | use reqwest::{Request, Response}; 9 | use reqwest_middleware::{Error, Middleware, Next, Result}; 10 | use retry_policies::RetryPolicy; 11 | 12 | #[doc(hidden)] 13 | // We need this macro because tracing expects the level to be const: 14 | // https://github.com/tokio-rs/tracing/issues/2730 15 | #[cfg(feature = "tracing")] 16 | macro_rules! log_retry { 17 | ($level:expr, $($args:tt)*) => {{ 18 | match $level { 19 | ::tracing::Level::TRACE => ::tracing::trace!($($args)*), 20 | ::tracing::Level::DEBUG => ::tracing::debug!($($args)*), 21 | ::tracing::Level::INFO => ::tracing::info!($($args)*), 22 | ::tracing::Level::WARN => ::tracing::warn!($($args)*), 23 | ::tracing::Level::ERROR => ::tracing::error!($($args)*), 24 | } 25 | }}; 26 | } 27 | 28 | /// `RetryTransientMiddleware` offers retry logic for requests that fail in a transient manner 29 | /// and can be safely executed again. 30 | /// 31 | /// Currently, it allows setting a [RetryPolicy] algorithm for calculating the __wait_time__ 32 | /// between each request retry. Sleeping on non-`wasm32` archs is performed using 33 | /// [`tokio::time::sleep`], therefore it will respect pauses/auto-advance if run under a 34 | /// runtime that supports them. 35 | /// 36 | ///```rust 37 | /// use std::time::Duration; 38 | /// use reqwest_middleware::ClientBuilder; 39 | /// use retry_policies::{RetryDecision, RetryPolicy, Jitter}; 40 | /// use retry_policies::policies::ExponentialBackoff; 41 | /// use reqwest_retry::RetryTransientMiddleware; 42 | /// use reqwest::Client; 43 | /// 44 | /// // We create a ExponentialBackoff retry policy which implements `RetryPolicy`. 45 | /// let retry_policy = ExponentialBackoff::builder() 46 | /// .retry_bounds(Duration::from_secs(1), Duration::from_secs(60)) 47 | /// .jitter(Jitter::Bounded) 48 | /// .base(2) 49 | /// .build_with_total_retry_duration(Duration::from_secs(24 * 60 * 60)); 50 | /// 51 | /// let retry_transient_middleware = RetryTransientMiddleware::new_with_policy(retry_policy); 52 | /// let client = ClientBuilder::new(Client::new()).with(retry_transient_middleware).build(); 53 | ///``` 54 | /// 55 | /// # Note 56 | /// 57 | /// This middleware always errors when given requests with streaming bodies, before even executing 58 | /// the request. When this happens you'll get an [`Error::Middleware`] with the message 59 | /// 'Request object is not cloneable. Are you passing a streaming body?'. 60 | /// 61 | /// Some workaround suggestions: 62 | /// * If you can fit the data in memory, you can instead build static request bodies e.g. with 63 | /// `Body`'s `From` or `From` implementations. 64 | /// * You can wrap this middleware in a custom one which skips retries for streaming requests. 65 | /// * You can write a custom retry middleware that builds new streaming requests from the data 66 | /// source directly, avoiding the issue of streaming requests not being cloneable. 67 | pub struct RetryTransientMiddleware< 68 | T: RetryPolicy + Send + Sync + 'static, 69 | R: RetryableStrategy + Send + Sync + 'static = DefaultRetryableStrategy, 70 | > { 71 | retry_policy: T, 72 | retryable_strategy: R, 73 | #[cfg(feature = "tracing")] 74 | retry_log_level: tracing::Level, 75 | } 76 | 77 | impl RetryTransientMiddleware { 78 | /// Construct `RetryTransientMiddleware` with a [retry_policy][RetryPolicy]. 79 | pub fn new_with_policy(retry_policy: T) -> Self { 80 | Self::new_with_policy_and_strategy(retry_policy, DefaultRetryableStrategy) 81 | } 82 | 83 | /// Set the log [level][tracing::Level] for retry events. 84 | /// The default is [`WARN`][tracing::Level::WARN]. 85 | #[cfg(feature = "tracing")] 86 | pub fn with_retry_log_level(mut self, level: tracing::Level) -> Self { 87 | self.retry_log_level = level; 88 | self 89 | } 90 | } 91 | 92 | impl RetryTransientMiddleware 93 | where 94 | T: RetryPolicy + Send + Sync, 95 | R: RetryableStrategy + Send + Sync, 96 | { 97 | /// Construct `RetryTransientMiddleware` with a [retry_policy][RetryPolicy] and [retryable_strategy](RetryableStrategy). 98 | pub fn new_with_policy_and_strategy(retry_policy: T, retryable_strategy: R) -> Self { 99 | Self { 100 | retry_policy, 101 | retryable_strategy, 102 | #[cfg(feature = "tracing")] 103 | retry_log_level: tracing::Level::WARN, 104 | } 105 | } 106 | } 107 | 108 | #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] 109 | #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] 110 | impl Middleware for RetryTransientMiddleware 111 | where 112 | T: RetryPolicy + Send + Sync, 113 | R: RetryableStrategy + Send + Sync + 'static, 114 | { 115 | async fn handle( 116 | &self, 117 | req: Request, 118 | extensions: &mut Extensions, 119 | next: Next<'_>, 120 | ) -> Result { 121 | // TODO: Ideally we should create a new instance of the `Extensions` map to pass 122 | // downstream. This will guard against previous retries polluting `Extensions`. 123 | // That is, we only return what's populated in the typemap for the last retry attempt 124 | // and copy those into the the `global` Extensions map. 125 | self.execute_with_retry(req, next, extensions).await 126 | } 127 | } 128 | 129 | impl RetryTransientMiddleware 130 | where 131 | T: RetryPolicy + Send + Sync, 132 | R: RetryableStrategy + Send + Sync, 133 | { 134 | /// This function will try to execute the request, if it fails 135 | /// with an error classified as transient it will call itself 136 | /// to retry the request. 137 | async fn execute_with_retry<'a>( 138 | &'a self, 139 | req: Request, 140 | next: Next<'a>, 141 | ext: &'a mut Extensions, 142 | ) -> Result { 143 | let mut n_past_retries = 0; 144 | let start_time = SystemTime::now(); 145 | loop { 146 | // Cloning the request object before-the-fact is not ideal.. 147 | // However, if the body of the request is not static, e.g of type `Bytes`, 148 | // the Clone operation should be of constant complexity and not O(N) 149 | // since the byte abstraction is a shared pointer over a buffer. 150 | let duplicate_request = req.try_clone().ok_or_else(|| { 151 | Error::Middleware(anyhow!( 152 | "Request object is not cloneable. Are you passing a streaming body?" 153 | .to_string() 154 | )) 155 | })?; 156 | 157 | let result = next.clone().run(duplicate_request, ext).await; 158 | 159 | // We classify the response which will return None if not 160 | // errors were returned. 161 | if let Some(Retryable::Transient) = self.retryable_strategy.handle(&result) { 162 | // If the response failed and the error type was transient 163 | // we can safely try to retry the request. 164 | let retry_decision = self.retry_policy.should_retry(start_time, n_past_retries); 165 | if let retry_policies::RetryDecision::Retry { execute_after } = retry_decision { 166 | let duration = execute_after 167 | .duration_since(SystemTime::now()) 168 | .unwrap_or_else(|_| Duration::default()); 169 | // Sleep the requested amount before we try again. 170 | #[cfg(feature = "tracing")] 171 | log_retry!( 172 | self.retry_log_level, 173 | "Retry attempt #{}. Sleeping {:?} before the next attempt", 174 | n_past_retries, 175 | duration 176 | ); 177 | #[cfg(not(target_arch = "wasm32"))] 178 | tokio::time::sleep(duration).await; 179 | #[cfg(target_arch = "wasm32")] 180 | wasmtimer::tokio::sleep(duration).await; 181 | 182 | n_past_retries += 1; 183 | continue; 184 | } 185 | }; 186 | 187 | // Report whether we failed with or without retries. 188 | break if n_past_retries > 0 { 189 | result.map_err(|err| { 190 | Error::Middleware( 191 | RetryError::WithRetries { 192 | retries: n_past_retries, 193 | err, 194 | } 195 | .into(), 196 | ) 197 | }) 198 | } else { 199 | result.map_err(|err| Error::Middleware(RetryError::Error(err).into())) 200 | }; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /reqwest-tracing/src/reqwest_otel_span_macro.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | /// [`reqwest_otel_span!`](crate::reqwest_otel_span) creates a new [`tracing::Span`]. 3 | /// It empowers you to add custom properties to the span on top of the default properties provided by the macro 4 | /// 5 | /// Default Fields: 6 | /// - http.request.method 7 | /// - url.scheme 8 | /// - server.address 9 | /// - server.port 10 | /// - otel.kind 11 | /// - otel.name 12 | /// - otel.status_code 13 | /// - user_agent.original 14 | /// - http.response.status_code 15 | /// - error.message 16 | /// - error.cause_chain 17 | /// 18 | /// Here are some convenient functions to checkout [`default_on_request_success`], [`default_on_request_failure`], 19 | /// and [`default_on_request_end`]. 20 | /// 21 | /// # Why a macro? 22 | /// 23 | /// [`tracing`] requires all the properties attached to a span to be declared upfront, when the span is created. 24 | /// You cannot add new ones afterwards. 25 | /// This makes it extremely fast, but it pushes us to reach for macros when we need some level of composition. 26 | /// 27 | /// # Macro syntax 28 | /// 29 | /// The first argument is a [span name](https://opentelemetry.io/docs/reference/specification/trace/api/#span). 30 | /// The second argument passed to [`reqwest_otel_span!`](crate::reqwest_otel_span) is a reference to an [`reqwest::Request`]. 31 | /// 32 | /// ```rust 33 | /// use reqwest_middleware::Result; 34 | /// use http::Extensions; 35 | /// use reqwest::{Request, Response}; 36 | /// use reqwest_tracing::{ 37 | /// default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend 38 | /// }; 39 | /// use tracing::Span; 40 | /// 41 | /// pub struct CustomReqwestOtelSpanBackend; 42 | /// 43 | /// impl ReqwestOtelSpanBackend for CustomReqwestOtelSpanBackend { 44 | /// fn on_request_start(req: &Request, _extension: &mut Extensions) -> Span { 45 | /// reqwest_otel_span!(name = "reqwest-http-request", req) 46 | /// } 47 | /// 48 | /// fn on_request_end(span: &Span, outcome: &Result, _extension: &mut Extensions) { 49 | /// default_on_request_end(span, outcome) 50 | /// } 51 | /// } 52 | /// ``` 53 | /// 54 | /// If nothing else is specified, the span generated by `reqwest_otel_span!` is identical to the one you'd 55 | /// get by using [`DefaultSpanBackend`]. Note that to avoid leaking sensitive information, the 56 | /// macro doesn't include `url.full`, even though it's required by opentelemetry. You can add the 57 | /// URL attribute explicitly by using [`SpanBackendWithUrl`] instead of `DefaultSpanBackend` or 58 | /// adding the field on your own implementation. 59 | /// 60 | /// You can define new fields following the same syntax of [`tracing::info_span!`] for fields: 61 | /// 62 | /// ```rust,should_panic 63 | /// use reqwest_tracing::reqwest_otel_span; 64 | /// # let request: &reqwest::Request = todo!(); 65 | /// 66 | /// // Define a `time_elapsed` field as empty. It might be populated later. 67 | /// // (This example is just to show how to inject data - otel already tracks durations) 68 | /// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty); 69 | /// 70 | /// // Define a `name` field with a known value, `AppName`. 71 | /// reqwest_otel_span!(name = "reqwest-http-request", request, name = "AppName"); 72 | /// 73 | /// // Define an `app_id` field using the variable with the same name as value. 74 | /// let app_id = "XYZ"; 75 | /// reqwest_otel_span!(name = "reqwest-http-request", request, app_id); 76 | /// 77 | /// // All together 78 | /// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty, name = "AppName", app_id); 79 | /// ``` 80 | /// 81 | /// You can also choose to customise the level of the generated span: 82 | /// 83 | /// ```rust,should_panic 84 | /// use reqwest_tracing::reqwest_otel_span; 85 | /// use tracing::Level; 86 | /// # let request: &reqwest::Request = todo!(); 87 | /// 88 | /// // Reduce the log level for service endpoints/probes 89 | /// let level = if request.method().as_str() == "POST" { 90 | /// Level::DEBUG 91 | /// } else { 92 | /// Level::INFO 93 | /// }; 94 | /// 95 | /// // `level =` and name MUST come before the request, in this order 96 | /// reqwest_otel_span!(level = level, name = "reqwest-http-request", request); 97 | /// ``` 98 | /// 99 | /// 100 | /// [`DefaultSpanBackend`]: crate::reqwest_otel_span_builder::DefaultSpanBackend 101 | /// [`SpanBackendWithUrl`]: crate::reqwest_otel_span_builder::DefaultSpanBackend 102 | /// [`default_on_request_success`]: crate::reqwest_otel_span_builder::default_on_request_success 103 | /// [`default_on_request_failure`]: crate::reqwest_otel_span_builder::default_on_request_failure 104 | /// [`default_on_request_end`]: crate::reqwest_otel_span_builder::default_on_request_end 105 | macro_rules! reqwest_otel_span { 106 | // Vanilla root span at default INFO level, with no additional fields 107 | (name=$name:expr, $request:ident) => { 108 | reqwest_otel_span!(name=$name, $request,) 109 | }; 110 | // Vanilla root span, with no additional fields but custom level 111 | (level=$level:expr, name=$name:expr, $request:ident) => { 112 | reqwest_otel_span!(level=$level, name=$name, $request,) 113 | }; 114 | // Root span with additional fields, default INFO level 115 | (name=$name:expr, $request:ident, $($field:tt)*) => { 116 | reqwest_otel_span!(level=$crate::reqwest_otel_span_macro::private::Level::INFO, name=$name, $request, $($field)*) 117 | }; 118 | // Root span with additional fields and custom level 119 | (level=$level:expr, name=$name:expr, $request:ident, $($field:tt)*) => { 120 | { 121 | let method = $request.method(); 122 | let url = $request.url(); 123 | let scheme = url.scheme(); 124 | let host = url.host_str().unwrap_or(""); 125 | let host_port = url.port_or_known_default().unwrap_or(0) as i64; 126 | let otel_name = $name.to_string(); 127 | let header_default = &::http::HeaderValue::from_static(""); 128 | let user_agent = format!("{:?}", $request.headers().get("user-agent").unwrap_or(header_default)).replace('"', ""); 129 | 130 | // The match here is necessary, because tracing expects the level to be static. 131 | match $level { 132 | $crate::reqwest_otel_span_macro::private::Level::TRACE => { 133 | $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::TRACE, method, scheme, host, host_port, user_agent, otel_name, $($field)*) 134 | }, 135 | $crate::reqwest_otel_span_macro::private::Level::DEBUG => { 136 | $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::DEBUG, method, scheme, host, host_port, user_agent, otel_name, $($field)*) 137 | }, 138 | $crate::reqwest_otel_span_macro::private::Level::INFO => { 139 | $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::INFO, method, scheme, host, host_port, user_agent, otel_name, $($field)*) 140 | }, 141 | $crate::reqwest_otel_span_macro::private::Level::WARN => { 142 | $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::WARN, method, scheme, host, host_port, user_agent, otel_name, $($field)*) 143 | }, 144 | $crate::reqwest_otel_span_macro::private::Level::ERROR => { 145 | $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::ERROR, method, scheme, host, host_port, user_agent, otel_name, $($field)*) 146 | }, 147 | } 148 | } 149 | } 150 | } 151 | 152 | #[doc(hidden)] 153 | pub mod private { 154 | #[doc(hidden)] 155 | pub use tracing::{span, Level}; 156 | 157 | #[cfg(not(feature = "deprecated_attributes"))] 158 | #[doc(hidden)] 159 | #[macro_export] 160 | macro_rules! request_span { 161 | ($level:expr, $method:expr, $scheme:expr, $host:expr, $host_port:expr, $user_agent:expr, $otel_name:expr, $($field:tt)*) => { 162 | $crate::reqwest_otel_span_macro::private::span!( 163 | $level, 164 | "HTTP request", 165 | http.request.method = %$method, 166 | url.scheme = %$scheme, 167 | server.address = %$host, 168 | server.port = %$host_port, 169 | user_agent.original = %$user_agent, 170 | otel.kind = "client", 171 | otel.name = %$otel_name, 172 | otel.status_code = tracing::field::Empty, 173 | http.response.status_code = tracing::field::Empty, 174 | error.message = tracing::field::Empty, 175 | error.cause_chain = tracing::field::Empty, 176 | $($field)* 177 | ) 178 | } 179 | } 180 | 181 | // With the deprecated attributes flag enabled, we publish both the old and new attributes. 182 | #[cfg(feature = "deprecated_attributes")] 183 | #[doc(hidden)] 184 | #[macro_export] 185 | macro_rules! request_span { 186 | ($level:expr, $method:expr, $scheme:expr, $host:expr, $host_port:expr, $user_agent:expr, $otel_name:expr, $($field:tt)*) => { 187 | $crate::reqwest_otel_span_macro::private::span!( 188 | $level, 189 | "HTTP request", 190 | http.request.method = %$method, 191 | url.scheme = %$scheme, 192 | server.address = %$host, 193 | server.port = %$host_port, 194 | user_agent.original = %$user_agent, 195 | otel.kind = "client", 196 | otel.name = %$otel_name, 197 | otel.status_code = tracing::field::Empty, 198 | http.response.status_code = tracing::field::Empty, 199 | error.message = tracing::field::Empty, 200 | error.cause_chain = tracing::field::Empty, 201 | // old attributes 202 | http.method = %$method, 203 | http.scheme = %$scheme, 204 | http.host = %$host, 205 | net.host.port = %$host_port, 206 | http.user_agent = tracing::field::Empty, 207 | http.status_code = tracing::field::Empty, 208 | $($field)* 209 | ) 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /reqwest-retry/tests/all/retry.rs: -------------------------------------------------------------------------------- 1 | use futures::FutureExt; 2 | use paste::paste; 3 | use reqwest::Client; 4 | use reqwest::StatusCode; 5 | use reqwest_middleware::ClientBuilder; 6 | use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; 7 | use std::sync::atomic::AtomicI8; 8 | use std::sync::{ 9 | atomic::{AtomicU32, Ordering}, 10 | Arc, 11 | }; 12 | use tokio::io::AsyncReadExt; 13 | use tokio::io::AsyncWriteExt; 14 | use wiremock::matchers::{method, path}; 15 | use wiremock::{Mock, MockServer, Respond, ResponseTemplate}; 16 | 17 | use crate::helpers::SimpleServer; 18 | pub struct RetryResponder(Arc, u32, u16); 19 | 20 | impl RetryResponder { 21 | fn new(retries: u32, status_code: u16) -> Self { 22 | Self(Arc::new(AtomicU32::new(0)), retries, status_code) 23 | } 24 | } 25 | 26 | impl Respond for RetryResponder { 27 | fn respond(&self, _request: &wiremock::Request) -> ResponseTemplate { 28 | let mut retries = self.0.load(Ordering::SeqCst); 29 | retries += 1; 30 | self.0.store(retries, Ordering::SeqCst); 31 | 32 | if retries + 1 >= self.1 { 33 | ResponseTemplate::new(200) 34 | } else { 35 | ResponseTemplate::new(self.2) 36 | } 37 | } 38 | } 39 | 40 | macro_rules! assert_retry_succeeds_inner { 41 | ($x:tt, $name:ident, $status:expr, $retry:tt, $exact:tt, $responder:expr) => { 42 | #[tokio::test] 43 | async fn $name() { 44 | let server = MockServer::start().await; 45 | let retry_amount: u32 = $retry; 46 | Mock::given(method("GET")) 47 | .and(path("/foo")) 48 | .respond_with($responder) 49 | .expect($exact) 50 | .mount(&server) 51 | .await; 52 | 53 | let reqwest_client = Client::builder().build().unwrap(); 54 | let client = ClientBuilder::new(reqwest_client) 55 | .with(RetryTransientMiddleware::new_with_policy( 56 | ExponentialBackoff::builder() 57 | .retry_bounds( 58 | std::time::Duration::from_millis(30), 59 | std::time::Duration::from_millis(100), 60 | ) 61 | .build_with_max_retries(retry_amount), 62 | )) 63 | .build(); 64 | 65 | let resp = client 66 | .get(&format!("{}/foo", server.uri())) 67 | .send() 68 | .await 69 | .expect("call failed"); 70 | 71 | assert_eq!(resp.status(), $status); 72 | } 73 | }; 74 | } 75 | 76 | macro_rules! assert_retry_succeeds { 77 | ($x:tt, $status:expr) => { 78 | paste! { 79 | assert_retry_succeeds_inner!($x, [], $status, 3, 2, RetryResponder::new(3 as u32, $x)); 80 | } 81 | }; 82 | } 83 | 84 | macro_rules! assert_no_retry { 85 | ($x:tt, $status:expr) => { 86 | paste! { 87 | assert_retry_succeeds_inner!($x, [], $status, 1, 1, ResponseTemplate::new($x)); 88 | } 89 | }; 90 | } 91 | 92 | // 2xx. 93 | assert_no_retry!(200, StatusCode::OK); 94 | assert_no_retry!(201, StatusCode::CREATED); 95 | assert_no_retry!(202, StatusCode::ACCEPTED); 96 | assert_no_retry!(203, StatusCode::NON_AUTHORITATIVE_INFORMATION); 97 | assert_no_retry!(204, StatusCode::NO_CONTENT); 98 | assert_no_retry!(205, StatusCode::RESET_CONTENT); 99 | assert_no_retry!(206, StatusCode::PARTIAL_CONTENT); 100 | assert_no_retry!(207, StatusCode::MULTI_STATUS); 101 | assert_no_retry!(226, StatusCode::IM_USED); 102 | 103 | // 3xx. 104 | assert_no_retry!(300, StatusCode::MULTIPLE_CHOICES); 105 | assert_no_retry!(301, StatusCode::MOVED_PERMANENTLY); 106 | assert_no_retry!(302, StatusCode::FOUND); 107 | assert_no_retry!(303, StatusCode::SEE_OTHER); 108 | assert_no_retry!(304, StatusCode::NOT_MODIFIED); 109 | assert_no_retry!(307, StatusCode::TEMPORARY_REDIRECT); 110 | assert_no_retry!(308, StatusCode::PERMANENT_REDIRECT); 111 | 112 | // 5xx. 113 | assert_retry_succeeds!(500, StatusCode::OK); 114 | assert_retry_succeeds!(501, StatusCode::OK); 115 | assert_retry_succeeds!(502, StatusCode::OK); 116 | assert_retry_succeeds!(503, StatusCode::OK); 117 | assert_retry_succeeds!(504, StatusCode::OK); 118 | assert_retry_succeeds!(505, StatusCode::OK); 119 | assert_retry_succeeds!(506, StatusCode::OK); 120 | assert_retry_succeeds!(507, StatusCode::OK); 121 | assert_retry_succeeds!(508, StatusCode::OK); 122 | assert_retry_succeeds!(510, StatusCode::OK); 123 | assert_retry_succeeds!(511, StatusCode::OK); 124 | // 4xx. 125 | assert_no_retry!(400, StatusCode::BAD_REQUEST); 126 | assert_no_retry!(401, StatusCode::UNAUTHORIZED); 127 | assert_no_retry!(402, StatusCode::PAYMENT_REQUIRED); 128 | assert_no_retry!(403, StatusCode::FORBIDDEN); 129 | assert_no_retry!(404, StatusCode::NOT_FOUND); 130 | assert_no_retry!(405, StatusCode::METHOD_NOT_ALLOWED); 131 | assert_no_retry!(406, StatusCode::NOT_ACCEPTABLE); 132 | assert_no_retry!(407, StatusCode::PROXY_AUTHENTICATION_REQUIRED); 133 | assert_retry_succeeds!(408, StatusCode::OK); 134 | assert_no_retry!(409, StatusCode::CONFLICT); 135 | assert_no_retry!(410, StatusCode::GONE); 136 | assert_no_retry!(411, StatusCode::LENGTH_REQUIRED); 137 | assert_no_retry!(412, StatusCode::PRECONDITION_FAILED); 138 | assert_no_retry!(413, StatusCode::PAYLOAD_TOO_LARGE); 139 | assert_no_retry!(414, StatusCode::URI_TOO_LONG); 140 | assert_no_retry!(415, StatusCode::UNSUPPORTED_MEDIA_TYPE); 141 | assert_no_retry!(416, StatusCode::RANGE_NOT_SATISFIABLE); 142 | assert_no_retry!(417, StatusCode::EXPECTATION_FAILED); 143 | assert_no_retry!(418, StatusCode::IM_A_TEAPOT); 144 | assert_no_retry!(421, StatusCode::MISDIRECTED_REQUEST); 145 | assert_no_retry!(422, StatusCode::UNPROCESSABLE_ENTITY); 146 | assert_no_retry!(423, StatusCode::LOCKED); 147 | assert_no_retry!(424, StatusCode::FAILED_DEPENDENCY); 148 | assert_no_retry!(426, StatusCode::UPGRADE_REQUIRED); 149 | assert_no_retry!(428, StatusCode::PRECONDITION_REQUIRED); 150 | assert_retry_succeeds!(429, StatusCode::OK); 151 | assert_no_retry!(431, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); 152 | assert_no_retry!(451, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); 153 | 154 | pub struct RetryTimeoutResponder(Arc, u32, std::time::Duration); 155 | 156 | impl RetryTimeoutResponder { 157 | fn new(retries: u32, initial_timeout: std::time::Duration) -> Self { 158 | Self(Arc::new(AtomicU32::new(0)), retries, initial_timeout) 159 | } 160 | } 161 | 162 | impl Respond for RetryTimeoutResponder { 163 | fn respond(&self, _request: &wiremock::Request) -> ResponseTemplate { 164 | let mut retries = self.0.load(Ordering::SeqCst); 165 | retries += 1; 166 | self.0.store(retries, Ordering::SeqCst); 167 | 168 | if retries + 1 >= self.1 { 169 | ResponseTemplate::new(200) 170 | } else { 171 | ResponseTemplate::new(500).set_delay(self.2) 172 | } 173 | } 174 | } 175 | 176 | #[tokio::test] 177 | async fn assert_retry_on_request_timeout() { 178 | let server = MockServer::start().await; 179 | Mock::given(method("GET")) 180 | .and(path("/foo")) 181 | .respond_with(RetryTimeoutResponder::new( 182 | 3, 183 | std::time::Duration::from_millis(1000), 184 | )) 185 | .expect(2) 186 | .mount(&server) 187 | .await; 188 | 189 | let reqwest_client = Client::builder().build().unwrap(); 190 | let client = ClientBuilder::new(reqwest_client) 191 | .with(RetryTransientMiddleware::new_with_policy( 192 | ExponentialBackoff::builder() 193 | .retry_bounds( 194 | std::time::Duration::from_millis(30), 195 | std::time::Duration::from_millis(100), 196 | ) 197 | .build_with_max_retries(3), 198 | )) 199 | .build(); 200 | 201 | let resp = client 202 | .get(format!("{}/foo", server.uri())) 203 | .timeout(std::time::Duration::from_millis(10)) 204 | .send() 205 | .await 206 | .expect("call failed"); 207 | 208 | assert_eq!(resp.status(), 200); 209 | } 210 | 211 | #[tokio::test] 212 | async fn assert_retry_on_incomplete_message() { 213 | // Following the HTTP/1.1 specification (https://en.wikipedia.org/wiki/HTTP_message_body) a valid response contains: 214 | // - status line 215 | // - headers 216 | // - empty line 217 | // - optional message body 218 | // 219 | // After a few tries we have noticed that: 220 | // - "message_that_makes_no_sense" triggers a hyper::ParseError because the format is completely wrong 221 | // - "HTTP/1.1" triggers a hyper::IncompleteMessage because the format is correct until that point but misses mandatory parts 222 | let incomplete_message = "HTTP/1.1"; 223 | let complete_message = "HTTP/1.1 200 OK\r\n\r\n"; 224 | 225 | // create a SimpleServer that returns the correct response after 3 attempts. 226 | // the first 3 attempts are incomplete http response and internally they result in a [`hyper::Error(IncompleteMessage)`] error. 227 | let simple_server = SimpleServer::new( 228 | "127.0.0.1", 229 | None, 230 | vec![ 231 | incomplete_message.to_string(), 232 | incomplete_message.to_string(), 233 | incomplete_message.to_string(), 234 | complete_message.to_string(), 235 | ], 236 | ) 237 | .await 238 | .expect("Error when creating a simple server"); 239 | 240 | let uri = simple_server.uri(); 241 | 242 | tokio::spawn(simple_server.start()); 243 | 244 | let reqwest_client = Client::builder().build().unwrap(); 245 | let client = ClientBuilder::new(reqwest_client) 246 | .with(RetryTransientMiddleware::new_with_policy( 247 | ExponentialBackoff::builder() 248 | .retry_bounds( 249 | std::time::Duration::from_millis(30), 250 | std::time::Duration::from_millis(100), 251 | ) 252 | .build_with_max_retries(3), 253 | )) 254 | .build(); 255 | 256 | let resp = client 257 | .get(format!("{}/foo", uri)) 258 | .timeout(std::time::Duration::from_millis(100)) 259 | .send() 260 | .await 261 | .expect("call failed"); 262 | 263 | assert_eq!(resp.status(), 200); 264 | } 265 | 266 | #[tokio::test] 267 | async fn assert_retry_on_hyper_canceled() { 268 | let counter = Arc::new(AtomicI8::new(0)); 269 | let mut simple_server = SimpleServer::new("127.0.0.1", None, vec![]) 270 | .await 271 | .expect("Error when creating a simple server"); 272 | simple_server.set_custom_handler(move |mut stream| { 273 | let counter = counter.clone(); 274 | async move { 275 | let mut buffer = Vec::new(); 276 | stream.read_buf(&mut buffer).await.unwrap(); 277 | if counter.fetch_add(1, Ordering::SeqCst) > 1 { 278 | // This triggers hyper:Error(Canceled). 279 | let _res = stream 280 | .into_std() 281 | .unwrap() 282 | .shutdown(std::net::Shutdown::Both); 283 | } else { 284 | let _res = stream.write("HTTP/1.1 200 OK\r\n\r\n".as_bytes()).await; 285 | } 286 | Ok(()) 287 | } 288 | .boxed() 289 | }); 290 | 291 | let uri = simple_server.uri(); 292 | 293 | tokio::spawn(simple_server.start()); 294 | 295 | let reqwest_client = Client::builder().build().unwrap(); 296 | let client = ClientBuilder::new(reqwest_client) 297 | .with(RetryTransientMiddleware::new_with_policy( 298 | ExponentialBackoff::builder() 299 | .retry_bounds( 300 | std::time::Duration::from_millis(30), 301 | std::time::Duration::from_millis(100), 302 | ) 303 | .build_with_max_retries(3), 304 | )) 305 | .build(); 306 | 307 | let resp = client 308 | .get(format!("{}/foo", uri)) 309 | .timeout(std::time::Duration::from_millis(100)) 310 | .send() 311 | .await 312 | .expect("call failed"); 313 | 314 | assert_eq!(resp.status(), 200); 315 | } 316 | 317 | #[tokio::test] 318 | async fn assert_retry_on_connection_reset_by_peer() { 319 | let counter = Arc::new(AtomicI8::new(0)); 320 | let mut simple_server = SimpleServer::new("127.0.0.1", None, vec![]) 321 | .await 322 | .expect("Error when creating a simple server"); 323 | simple_server.set_custom_handler(move |mut stream| { 324 | let counter = counter.clone(); 325 | async move { 326 | let mut buffer = Vec::new(); 327 | stream.read_buf(&mut buffer).await.unwrap(); 328 | if counter.fetch_add(1, Ordering::SeqCst) > 1 { 329 | // This triggers hyper:Error(Io, io::Error(ConnectionReset)). 330 | drop(stream); 331 | } else { 332 | let _res = stream.write("HTTP/1.1 200 OK\r\n\r\n".as_bytes()).await; 333 | } 334 | Ok(()) 335 | } 336 | .boxed() 337 | }); 338 | 339 | let uri = simple_server.uri(); 340 | 341 | tokio::spawn(simple_server.start()); 342 | 343 | let reqwest_client = Client::builder().build().unwrap(); 344 | let client = ClientBuilder::new(reqwest_client) 345 | .with(RetryTransientMiddleware::new_with_policy( 346 | ExponentialBackoff::builder() 347 | .retry_bounds( 348 | std::time::Duration::from_millis(30), 349 | std::time::Duration::from_millis(100), 350 | ) 351 | .build_with_max_retries(3), 352 | )) 353 | .build(); 354 | 355 | let resp = client 356 | .get(format!("{}/foo", uri)) 357 | .timeout(std::time::Duration::from_millis(100)) 358 | .send() 359 | .await 360 | .expect("call failed"); 361 | 362 | assert_eq!(resp.status(), 200); 363 | } 364 | -------------------------------------------------------------------------------- /reqwest-tracing/src/reqwest_otel_span_builder.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use http::Extensions; 4 | use matchit::Router; 5 | use reqwest::{Request, Response, StatusCode as RequestStatusCode, Url}; 6 | use reqwest_middleware::{Error, Result}; 7 | use tracing::{warn, Span}; 8 | 9 | use crate::reqwest_otel_span; 10 | 11 | /// The `http.request.method` field added to the span by [`reqwest_otel_span`] 12 | pub const HTTP_REQUEST_METHOD: &str = "http.request.method"; 13 | /// The `url.scheme` field added to the span by [`reqwest_otel_span`] 14 | pub const URL_SCHEME: &str = "url.scheme"; 15 | /// The `server.address` field added to the span by [`reqwest_otel_span`] 16 | pub const SERVER_ADDRESS: &str = "server.address"; 17 | /// The `server.port` field added to the span by [`reqwest_otel_span`] 18 | pub const SERVER_PORT: &str = "server.port"; 19 | /// The `url.full` field added to the span by [`reqwest_otel_span`] 20 | pub const URL_FULL: &str = "url.full"; 21 | /// The `user_agent.original` field added to the span by [`reqwest_otel_span`] 22 | pub const USER_AGENT_ORIGINAL: &str = "user_agent.original"; 23 | /// The `otel.kind` field added to the span by [`reqwest_otel_span`] 24 | pub const OTEL_KIND: &str = "otel.kind"; 25 | /// The `otel.name` field added to the span by [`reqwest_otel_span`] 26 | pub const OTEL_NAME: &str = "otel.name"; 27 | /// The `otel.status_code` field added to the span by [`reqwest_otel_span`] 28 | pub const OTEL_STATUS_CODE: &str = "otel.status_code"; 29 | /// The `http.response.status_code` field added to the span by [`reqwest_otel_span`] 30 | pub const HTTP_RESPONSE_STATUS_CODE: &str = "http.response.status_code"; 31 | /// The `error.message` field added to the span by [`reqwest_otel_span`] 32 | pub const ERROR_MESSAGE: &str = "error.message"; 33 | /// The `error.cause_chain` field added to the span by [`reqwest_otel_span`] 34 | pub const ERROR_CAUSE_CHAIN: &str = "error.cause_chain"; 35 | 36 | /// The `http.method` field added to the span by [`reqwest_otel_span`] 37 | #[cfg(feature = "deprecated_attributes")] 38 | pub const HTTP_METHOD: &str = "http.method"; 39 | /// The `http.scheme` field added to the span by [`reqwest_otel_span`] 40 | #[cfg(feature = "deprecated_attributes")] 41 | pub const HTTP_SCHEME: &str = "http.scheme"; 42 | /// The `http.host` field added to the span by [`reqwest_otel_span`] 43 | #[cfg(feature = "deprecated_attributes")] 44 | pub const HTTP_HOST: &str = "http.host"; 45 | /// The `http.url` field added to the span by [`reqwest_otel_span`] 46 | #[cfg(feature = "deprecated_attributes")] 47 | pub const HTTP_URL: &str = "http.url"; 48 | /// The `host.port` field added to the span by [`reqwest_otel_span`] 49 | #[cfg(feature = "deprecated_attributes")] 50 | pub const NET_HOST_PORT: &str = "net.host.port"; 51 | /// The `http.status_code` field added to the span by [`reqwest_otel_span`] 52 | #[cfg(feature = "deprecated_attributes")] 53 | pub const HTTP_STATUS_CODE: &str = "http.status_code"; 54 | /// The `http.user_agent` added to the span by [`reqwest_otel_span`] 55 | #[cfg(feature = "deprecated_attributes")] 56 | pub const HTTP_USER_AGENT: &str = "http.user_agent"; 57 | 58 | /// [`ReqwestOtelSpanBackend`] allows you to customise the span attached by 59 | /// [`TracingMiddleware`] to incoming requests. 60 | /// 61 | /// Check out [`reqwest_otel_span`] documentation for examples. 62 | /// 63 | /// [`TracingMiddleware`]: crate::middleware::TracingMiddleware. 64 | pub trait ReqwestOtelSpanBackend { 65 | /// Initialized a new span before the request is executed. 66 | fn on_request_start(req: &Request, extension: &mut Extensions) -> Span; 67 | 68 | /// Runs after the request call has executed. 69 | fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions); 70 | } 71 | 72 | /// Populates default success/failure fields for a given [`reqwest_otel_span!`] span. 73 | #[inline] 74 | pub fn default_on_request_end(span: &Span, outcome: &Result) { 75 | match outcome { 76 | Ok(res) => default_on_request_success(span, res), 77 | Err(err) => default_on_request_failure(span, err), 78 | } 79 | } 80 | 81 | #[cfg(feature = "deprecated_attributes")] 82 | fn get_header_value(key: &str, headers: &reqwest::header::HeaderMap) -> String { 83 | let header_default = &reqwest::header::HeaderValue::from_static(""); 84 | format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "") 85 | } 86 | 87 | /// Populates default success fields for a given [`reqwest_otel_span!`] span. 88 | #[inline] 89 | pub fn default_on_request_success(span: &Span, response: &Response) { 90 | let span_status = get_span_status(response.status()); 91 | if let Some(span_status) = span_status { 92 | span.record(OTEL_STATUS_CODE, span_status); 93 | } 94 | span.record(HTTP_RESPONSE_STATUS_CODE, response.status().as_u16()); 95 | #[cfg(feature = "deprecated_attributes")] 96 | { 97 | let user_agent = get_header_value("user_agent", response.headers()); 98 | span.record(HTTP_STATUS_CODE, response.status().as_u16()); 99 | span.record(HTTP_USER_AGENT, user_agent.as_str()); 100 | } 101 | } 102 | 103 | /// Populates default failure fields for a given [`reqwest_otel_span!`] span. 104 | #[inline] 105 | pub fn default_on_request_failure(span: &Span, e: &Error) { 106 | let error_message = e.to_string(); 107 | let error_cause_chain = format!("{:?}", e); 108 | span.record(OTEL_STATUS_CODE, "ERROR"); 109 | span.record(ERROR_MESSAGE, error_message.as_str()); 110 | span.record(ERROR_CAUSE_CHAIN, error_cause_chain.as_str()); 111 | if let Error::Reqwest(e) = e { 112 | if let Some(status) = e.status() { 113 | span.record(HTTP_RESPONSE_STATUS_CODE, status.as_u16()); 114 | #[cfg(feature = "deprecated_attributes")] 115 | { 116 | span.record(HTTP_STATUS_CODE, status.as_u16()); 117 | } 118 | } 119 | } 120 | } 121 | 122 | /// Determine the name of the span that should be associated with this request. 123 | /// 124 | /// This tries to be PII safe by default, not including any path information unless 125 | /// specifically opted in using either [`OtelName`] or [`OtelPathNames`] 126 | #[inline] 127 | pub fn default_span_name<'a>(req: &'a Request, ext: &'a Extensions) -> Cow<'a, str> { 128 | if let Some(name) = ext.get::() { 129 | Cow::Borrowed(name.0.as_ref()) 130 | } else if let Some(path_names) = ext.get::() { 131 | path_names 132 | .find(req.url().path()) 133 | .map(|path| Cow::Owned(format!("{} {}", req.method(), path))) 134 | .unwrap_or_else(|| { 135 | warn!("no OTEL path name found"); 136 | Cow::Owned(format!("{} UNKNOWN", req.method().as_str())) 137 | }) 138 | } else { 139 | Cow::Borrowed(req.method().as_str()) 140 | } 141 | } 142 | 143 | /// The default [`ReqwestOtelSpanBackend`] for [`TracingMiddleware`]. Note that it doesn't include 144 | /// the `url.full` field in spans, you can use [`SpanBackendWithUrl`] to add it. 145 | /// 146 | /// [`TracingMiddleware`]: crate::middleware::TracingMiddleware 147 | pub struct DefaultSpanBackend; 148 | 149 | impl ReqwestOtelSpanBackend for DefaultSpanBackend { 150 | fn on_request_start(req: &Request, ext: &mut Extensions) -> Span { 151 | let name = default_span_name(req, ext); 152 | reqwest_otel_span!(name = name, req) 153 | } 154 | 155 | fn on_request_end(span: &Span, outcome: &Result, _: &mut Extensions) { 156 | default_on_request_end(span, outcome) 157 | } 158 | } 159 | 160 | /// Similar to [`DefaultSpanBackend`] but also adds the `url.full` attribute to request spans. 161 | /// 162 | /// [`TracingMiddleware`]: crate::middleware::TracingMiddleware 163 | pub struct SpanBackendWithUrl; 164 | 165 | impl ReqwestOtelSpanBackend for SpanBackendWithUrl { 166 | fn on_request_start(req: &Request, ext: &mut Extensions) -> Span { 167 | let name = default_span_name(req, ext); 168 | let url = remove_credentials(req.url()); 169 | let span = reqwest_otel_span!(name = name, req, url.full = %url); 170 | #[cfg(feature = "deprecated_attributes")] 171 | { 172 | span.record(HTTP_URL, url.to_string()); 173 | } 174 | span 175 | } 176 | 177 | fn on_request_end(span: &Span, outcome: &Result, _: &mut Extensions) { 178 | default_on_request_end(span, outcome) 179 | } 180 | } 181 | 182 | /// HTTP Mapping 183 | /// 184 | /// Maps the the http status to an Opentelemetry span status following the the specified convention above. 185 | fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> { 186 | match request_status.as_u16() { 187 | // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, unless there was 188 | // another error (e.g., network error receiving the response body; or 3xx codes with max redirects exceeded), 189 | // in which case status MUST be set to Error. 190 | 100..=399 => None, 191 | // For HTTP status codes in the 4xx range span status MUST be left unset in case of SpanKind.SERVER and MUST be 192 | // set to Error in case of SpanKind.CLIENT. 193 | 400..=499 => Some("ERROR"), 194 | // For HTTP status codes in the 5xx range, as well as any other code the client failed to interpret, span 195 | // status MUST be set to Error. 196 | _ => Some("ERROR"), 197 | } 198 | } 199 | 200 | /// [`OtelName`] allows customisation of the name of the spans created by 201 | /// [`DefaultSpanBackend`] and [`SpanBackendWithUrl`]. 202 | /// 203 | /// Usage: 204 | /// ```no_run 205 | /// # use reqwest_middleware::Result; 206 | /// use reqwest_middleware::{ClientBuilder, Extension}; 207 | /// use reqwest_tracing::{ 208 | /// TracingMiddleware, OtelName 209 | /// }; 210 | /// # async fn example() -> Result<()> { 211 | /// let reqwest_client = reqwest::Client::builder().build().unwrap(); 212 | /// let client = ClientBuilder::new(reqwest_client) 213 | /// // Inserts the extension before the request is started 214 | /// .with_init(Extension(OtelName("my-client".into()))) 215 | /// // Makes use of that extension to specify the otel name 216 | /// .with(TracingMiddleware::default()) 217 | /// .build(); 218 | /// 219 | /// let resp = client.get("https://truelayer.com").send().await.unwrap(); 220 | /// 221 | /// // Or specify it on the individual request (will take priority) 222 | /// let resp = client.post("https://api.truelayer.com/payment") 223 | /// .with_extension(OtelName("POST /payment".into())) 224 | /// .send() 225 | /// .await 226 | /// .unwrap(); 227 | /// # Ok(()) 228 | /// # } 229 | /// ``` 230 | #[derive(Clone)] 231 | pub struct OtelName(pub Cow<'static, str>); 232 | 233 | /// [`OtelPathNames`] allows including templated paths in the spans created by 234 | /// [`DefaultSpanBackend`] and [`SpanBackendWithUrl`]. 235 | /// 236 | /// When creating spans this can be used to try to match the path against some 237 | /// known paths. If the path matches value returned is the templated path. This 238 | /// can be used in span names as it will not contain values that would 239 | /// increase the cardinality. 240 | /// 241 | /// ``` 242 | /// /// # use reqwest_middleware::Result; 243 | /// use reqwest_middleware::{ClientBuilder, Extension}; 244 | /// use reqwest_tracing::{ 245 | /// TracingMiddleware, OtelPathNames 246 | /// }; 247 | /// # async fn example() -> Result<(), Box> { 248 | /// let reqwest_client = reqwest::Client::builder().build()?; 249 | /// let client = ClientBuilder::new(reqwest_client) 250 | /// // Inserts the extension before the request is started 251 | /// .with_init(Extension(OtelPathNames::known_paths(["/payment/{paymentId}"])?)) 252 | /// // Makes use of that extension to specify the otel name 253 | /// .with(TracingMiddleware::default()) 254 | /// .build(); 255 | /// 256 | /// let resp = client.get("https://truelayer.com/payment/id-123").send().await?; 257 | /// 258 | /// // Or specify it on the individual request (will take priority) 259 | /// let resp = client.post("https://api.truelayer.com/payment/id-123/authorization-flow") 260 | /// .with_extension(OtelPathNames::known_paths(["/payment/{paymentId}/authorization-flow"])?) 261 | /// .send() 262 | /// .await?; 263 | /// # Ok(()) 264 | /// # } 265 | /// ``` 266 | #[derive(Clone)] 267 | pub struct OtelPathNames(matchit::Router); 268 | 269 | impl OtelPathNames { 270 | /// Create a new [`OtelPathNames`] from a set of known paths. 271 | /// 272 | /// Paths in this set will be found with `find`. 273 | /// 274 | /// Paths can have different parameters: 275 | /// - Named parameters like `:paymentId` match anything until the next `/` or the end of the path. 276 | /// - Catch-all parameters start with `*` and match everything after the `/`. They must be at the end of the route. 277 | /// ``` 278 | /// # use reqwest_tracing::OtelPathNames; 279 | /// OtelPathNames::known_paths([ 280 | /// "/", 281 | /// "/payment", 282 | /// "/payment/{paymentId}", 283 | /// "/payment/{paymentId}/*action", 284 | /// ]).unwrap(); 285 | /// ``` 286 | pub fn known_paths(paths: Paths) -> anyhow::Result 287 | where 288 | Paths: IntoIterator, 289 | Path: Into, 290 | { 291 | let mut router = Router::new(); 292 | for path in paths { 293 | let path = path.into(); 294 | router.insert(path.clone(), path)?; 295 | } 296 | 297 | Ok(Self(router)) 298 | } 299 | 300 | /// Find the templated path from the actual path. 301 | /// 302 | /// Returns the templated path if a match is found. 303 | /// 304 | /// ``` 305 | /// # use reqwest_tracing::OtelPathNames; 306 | /// let path_names = OtelPathNames::known_paths(["/payment/{paymentId}"]).unwrap(); 307 | /// let path = path_names.find("/payment/payment-id-123"); 308 | /// assert_eq!(path, Some("/payment/{paymentId}")); 309 | /// ``` 310 | pub fn find(&self, path: &str) -> Option<&str> { 311 | self.0.at(path).map(|mtch| mtch.value.as_str()).ok() 312 | } 313 | } 314 | 315 | /// `DisableOtelPropagation` disables opentelemetry header propagation, while still tracing the HTTP request. 316 | /// 317 | /// By default, the [`TracingMiddleware`](super::TracingMiddleware) middleware will also propagate any opentelemtry 318 | /// contexts to the server. For any external facing requests, this can be problematic and it should be disabled. 319 | /// 320 | /// Usage: 321 | /// ```no_run 322 | /// # use reqwest_middleware::Result; 323 | /// use reqwest_middleware::{ClientBuilder, Extension}; 324 | /// use reqwest_tracing::{ 325 | /// TracingMiddleware, DisableOtelPropagation 326 | /// }; 327 | /// # async fn example() -> Result<()> { 328 | /// let reqwest_client = reqwest::Client::builder().build().unwrap(); 329 | /// let client = ClientBuilder::new(reqwest_client) 330 | /// // Inserts the extension before the request is started 331 | /// .with_init(Extension(DisableOtelPropagation)) 332 | /// // Makes use of that extension to specify the otel name 333 | /// .with(TracingMiddleware::default()) 334 | /// .build(); 335 | /// 336 | /// let resp = client.get("https://truelayer.com").send().await.unwrap(); 337 | /// 338 | /// // Or specify it on the individual request (will take priority) 339 | /// let resp = client.post("https://api.truelayer.com/payment") 340 | /// .with_extension(DisableOtelPropagation) 341 | /// .send() 342 | /// .await 343 | /// .unwrap(); 344 | /// # Ok(()) 345 | /// # } 346 | /// ``` 347 | #[derive(Clone)] 348 | pub struct DisableOtelPropagation; 349 | 350 | /// Removes the username and/or password parts of the url, if present. 351 | fn remove_credentials(url: &Url) -> Cow<'_, str> { 352 | if !url.username().is_empty() || url.password().is_some() { 353 | let mut url = url.clone(); 354 | // Errors settings username/password are set when the URL can't have credentials, so 355 | // they're just ignored. 356 | url.set_username("") 357 | .and_then(|_| url.set_password(None)) 358 | .ok(); 359 | url.to_string().into() 360 | } else { 361 | url.as_ref().into() 362 | } 363 | } 364 | 365 | #[cfg(test)] 366 | mod tests { 367 | use super::*; 368 | 369 | use reqwest::header::{HeaderMap, HeaderValue}; 370 | 371 | fn get_header_value(key: &str, headers: &HeaderMap) -> String { 372 | let header_default = &HeaderValue::from_static(""); 373 | format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "") 374 | } 375 | 376 | #[test] 377 | fn get_header_value_for_span_attribute() { 378 | let expect = "IMPORTANT_HEADER"; 379 | let mut header_map = HeaderMap::new(); 380 | header_map.insert("test", expect.parse().unwrap()); 381 | 382 | let value = get_header_value("test", &header_map); 383 | assert_eq!(value, expect); 384 | } 385 | 386 | #[test] 387 | fn remove_credentials_from_url_without_credentials_is_noop() { 388 | let url = "http://nocreds.com/".parse().unwrap(); 389 | let clean = remove_credentials(&url); 390 | assert_eq!(clean, "http://nocreds.com/"); 391 | } 392 | 393 | #[test] 394 | fn remove_credentials_removes_username_only() { 395 | let url = "http://user@withuser.com/".parse().unwrap(); 396 | let clean = remove_credentials(&url); 397 | assert_eq!(clean, "http://withuser.com/"); 398 | } 399 | 400 | #[test] 401 | fn remove_credentials_removes_password_only() { 402 | let url = "http://:123@withpwd.com/".parse().unwrap(); 403 | let clean = remove_credentials(&url); 404 | assert_eq!(clean, "http://withpwd.com/"); 405 | } 406 | 407 | #[test] 408 | fn remove_credentials_removes_username_and_password() { 409 | let url = "http://user:123@both.com/".parse().unwrap(); 410 | let clean = remove_credentials(&url); 411 | assert_eq!(clean, "http://both.com/"); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /reqwest-tracing/src/otel.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::{HeaderName, HeaderValue}; 2 | use reqwest::Request; 3 | use std::str::FromStr; 4 | use tracing::Span; 5 | 6 | /// Injects the given OpenTelemetry Context into a reqwest::Request headers to allow propagation downstream. 7 | pub fn inject_opentelemetry_context_into_request(mut request: Request) -> Request { 8 | #[cfg(feature = "opentelemetry_0_20")] 9 | opentelemetry_0_20_pkg::global::get_text_map_propagator(|injector| { 10 | use tracing_opentelemetry_0_21_pkg::OpenTelemetrySpanExt; 11 | let context = Span::current().context(); 12 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 13 | }); 14 | 15 | #[cfg(feature = "opentelemetry_0_21")] 16 | opentelemetry_0_21_pkg::global::get_text_map_propagator(|injector| { 17 | use tracing_opentelemetry_0_22_pkg::OpenTelemetrySpanExt; 18 | let context = Span::current().context(); 19 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 20 | }); 21 | 22 | #[cfg(feature = "opentelemetry_0_22")] 23 | opentelemetry_0_22_pkg::global::get_text_map_propagator(|injector| { 24 | use tracing_opentelemetry_0_23_pkg::OpenTelemetrySpanExt; 25 | let context = Span::current().context(); 26 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 27 | }); 28 | 29 | #[cfg(feature = "opentelemetry_0_23")] 30 | opentelemetry_0_23_pkg::global::get_text_map_propagator(|injector| { 31 | use tracing_opentelemetry_0_24_pkg::OpenTelemetrySpanExt; 32 | let context = Span::current().context(); 33 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 34 | }); 35 | 36 | #[cfg(feature = "opentelemetry_0_24")] 37 | opentelemetry_0_24_pkg::global::get_text_map_propagator(|injector| { 38 | use tracing_opentelemetry_0_25_pkg::OpenTelemetrySpanExt; 39 | let context = Span::current().context(); 40 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 41 | }); 42 | 43 | #[cfg(feature = "opentelemetry_0_25")] 44 | opentelemetry_0_25_pkg::global::get_text_map_propagator(|injector| { 45 | use tracing_opentelemetry_0_26_pkg::OpenTelemetrySpanExt; 46 | let context = Span::current().context(); 47 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 48 | }); 49 | 50 | #[cfg(feature = "opentelemetry_0_26")] 51 | opentelemetry_0_26_pkg::global::get_text_map_propagator(|injector| { 52 | use tracing_opentelemetry_0_27_pkg::OpenTelemetrySpanExt; 53 | let context = Span::current().context(); 54 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 55 | }); 56 | 57 | #[cfg(feature = "opentelemetry_0_27")] 58 | opentelemetry_0_27_pkg::global::get_text_map_propagator(|injector| { 59 | use tracing_opentelemetry_0_28_pkg::OpenTelemetrySpanExt; 60 | let context = Span::current().context(); 61 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 62 | }); 63 | 64 | #[cfg(feature = "opentelemetry_0_28")] 65 | opentelemetry_0_28_pkg::global::get_text_map_propagator(|injector| { 66 | use tracing_opentelemetry_0_29_pkg::OpenTelemetrySpanExt; 67 | let context = Span::current().context(); 68 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 69 | }); 70 | 71 | #[cfg(feature = "opentelemetry_0_29")] 72 | opentelemetry_0_29_pkg::global::get_text_map_propagator(|injector| { 73 | use tracing_opentelemetry_0_30_pkg::OpenTelemetrySpanExt; 74 | let context = Span::current().context(); 75 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 76 | }); 77 | 78 | #[cfg(feature = "opentelemetry_0_30")] 79 | opentelemetry_0_30_pkg::global::get_text_map_propagator(|injector| { 80 | use tracing_opentelemetry_0_31_pkg::OpenTelemetrySpanExt; 81 | let context = Span::current().context(); 82 | injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) 83 | }); 84 | 85 | request 86 | } 87 | 88 | // "traceparent" => https://www.w3.org/TR/trace-context/#trace-context-http-headers-format 89 | 90 | /// Injector used via opentelemetry propagator to tell the extractor how to insert the "traceparent" header value 91 | /// This will allow the propagator to inject opentelemetry context into a standard data structure. Will basically 92 | /// insert a "traceparent" string value "{version}-{trace_id}-{span_id}-{trace-flags}" of the spans context into the headers. 93 | /// Listeners can then re-hydrate the context to add additional spans to the same trace. 94 | struct RequestCarrier<'a> { 95 | request: &'a mut Request, 96 | } 97 | 98 | impl<'a> RequestCarrier<'a> { 99 | pub fn new(request: &'a mut Request) -> Self { 100 | RequestCarrier { request } 101 | } 102 | } 103 | 104 | impl RequestCarrier<'_> { 105 | fn set_inner(&mut self, key: &str, value: String) { 106 | let header_name = HeaderName::from_str(key).expect("Must be header name"); 107 | let header_value = HeaderValue::from_str(&value).expect("Must be a header value"); 108 | self.request.headers_mut().insert(header_name, header_value); 109 | } 110 | } 111 | 112 | #[cfg(feature = "opentelemetry_0_20")] 113 | impl opentelemetry_0_20_pkg::propagation::Injector for RequestCarrier<'_> { 114 | fn set(&mut self, key: &str, value: String) { 115 | self.set_inner(key, value) 116 | } 117 | } 118 | 119 | #[cfg(feature = "opentelemetry_0_21")] 120 | impl opentelemetry_0_21_pkg::propagation::Injector for RequestCarrier<'_> { 121 | fn set(&mut self, key: &str, value: String) { 122 | self.set_inner(key, value) 123 | } 124 | } 125 | 126 | #[cfg(feature = "opentelemetry_0_22")] 127 | impl opentelemetry_0_22_pkg::propagation::Injector for RequestCarrier<'_> { 128 | fn set(&mut self, key: &str, value: String) { 129 | self.set_inner(key, value) 130 | } 131 | } 132 | 133 | #[cfg(feature = "opentelemetry_0_23")] 134 | impl opentelemetry_0_23_pkg::propagation::Injector for RequestCarrier<'_> { 135 | fn set(&mut self, key: &str, value: String) { 136 | self.set_inner(key, value) 137 | } 138 | } 139 | 140 | #[cfg(feature = "opentelemetry_0_24")] 141 | impl opentelemetry_0_24_pkg::propagation::Injector for RequestCarrier<'_> { 142 | fn set(&mut self, key: &str, value: String) { 143 | self.set_inner(key, value) 144 | } 145 | } 146 | 147 | #[cfg(feature = "opentelemetry_0_25")] 148 | impl opentelemetry_0_25_pkg::propagation::Injector for RequestCarrier<'_> { 149 | fn set(&mut self, key: &str, value: String) { 150 | self.set_inner(key, value) 151 | } 152 | } 153 | 154 | #[cfg(feature = "opentelemetry_0_26")] 155 | impl opentelemetry_0_26_pkg::propagation::Injector for RequestCarrier<'_> { 156 | fn set(&mut self, key: &str, value: String) { 157 | self.set_inner(key, value) 158 | } 159 | } 160 | 161 | #[cfg(feature = "opentelemetry_0_27")] 162 | impl opentelemetry_0_27_pkg::propagation::Injector for RequestCarrier<'_> { 163 | fn set(&mut self, key: &str, value: String) { 164 | self.set_inner(key, value) 165 | } 166 | } 167 | 168 | #[cfg(feature = "opentelemetry_0_28")] 169 | impl opentelemetry_0_28_pkg::propagation::Injector for RequestCarrier<'_> { 170 | fn set(&mut self, key: &str, value: String) { 171 | self.set_inner(key, value) 172 | } 173 | } 174 | 175 | #[cfg(feature = "opentelemetry_0_29")] 176 | impl opentelemetry_0_29_pkg::propagation::Injector for RequestCarrier<'_> { 177 | fn set(&mut self, key: &str, value: String) { 178 | self.set_inner(key, value) 179 | } 180 | } 181 | 182 | #[cfg(feature = "opentelemetry_0_30")] 183 | impl opentelemetry_0_30_pkg::propagation::Injector for RequestCarrier<'_> { 184 | fn set(&mut self, key: &str, value: String) { 185 | self.set_inner(key, value) 186 | } 187 | } 188 | 189 | #[cfg(test)] 190 | mod test { 191 | use std::sync::OnceLock; 192 | 193 | use crate::{DisableOtelPropagation, TracingMiddleware}; 194 | use reqwest::Response; 195 | use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, Extension}; 196 | use tracing::{info_span, Instrument, Level}; 197 | 198 | use tracing_subscriber::{filter, layer::SubscriberExt, Registry}; 199 | use wiremock::{matchers::any, Mock, MockServer, ResponseTemplate}; 200 | 201 | async fn make_echo_request_in_otel_context(client: ClientWithMiddleware) -> Response { 202 | static TELEMETRY: OnceLock<()> = OnceLock::new(); 203 | 204 | TELEMETRY.get_or_init(|| { 205 | let subscriber = Registry::default().with( 206 | filter::Targets::new().with_target("reqwest_tracing::otel::test", Level::DEBUG), 207 | ); 208 | 209 | #[cfg(feature = "opentelemetry_0_20")] 210 | let subscriber = { 211 | use opentelemetry_0_20_pkg::trace::TracerProvider; 212 | use opentelemetry_stdout_0_1::SpanExporterBuilder; 213 | 214 | let exporter = SpanExporterBuilder::default() 215 | .with_writer(std::io::sink()) 216 | .build(); 217 | 218 | let provider = opentelemetry_0_20_pkg::sdk::trace::TracerProvider::builder() 219 | .with_simple_exporter(exporter) 220 | .build(); 221 | 222 | let tracer = provider.versioned_tracer("reqwest", None::<&str>, None::<&str>, None); 223 | let _ = opentelemetry_0_20_pkg::global::set_tracer_provider(provider); 224 | opentelemetry_0_20_pkg::global::set_text_map_propagator( 225 | opentelemetry_0_20_pkg::sdk::propagation::TraceContextPropagator::new(), 226 | ); 227 | 228 | let telemetry = tracing_opentelemetry_0_21_pkg::layer().with_tracer(tracer); 229 | subscriber.with(telemetry) 230 | }; 231 | 232 | #[cfg(feature = "opentelemetry_0_21")] 233 | let subscriber = { 234 | use opentelemetry_0_21_pkg::trace::TracerProvider; 235 | use opentelemetry_stdout_0_2::SpanExporterBuilder; 236 | 237 | let exporter = SpanExporterBuilder::default() 238 | .with_writer(std::io::sink()) 239 | .build(); 240 | 241 | let provider = opentelemetry_sdk_0_21::trace::TracerProvider::builder() 242 | .with_simple_exporter(exporter) 243 | .build(); 244 | 245 | let tracer = provider.versioned_tracer("reqwest", None::<&str>, None::<&str>, None); 246 | let _ = opentelemetry_0_21_pkg::global::set_tracer_provider(provider); 247 | opentelemetry_0_21_pkg::global::set_text_map_propagator( 248 | opentelemetry_sdk_0_21::propagation::TraceContextPropagator::new(), 249 | ); 250 | 251 | let telemetry = tracing_opentelemetry_0_22_pkg::layer().with_tracer(tracer); 252 | subscriber.with(telemetry) 253 | }; 254 | 255 | #[cfg(feature = "opentelemetry_0_22")] 256 | let subscriber = { 257 | use opentelemetry_0_22_pkg::trace::TracerProvider; 258 | use opentelemetry_stdout_0_3::SpanExporterBuilder; 259 | 260 | let exporter = SpanExporterBuilder::default() 261 | .with_writer(std::io::sink()) 262 | .build(); 263 | 264 | let provider = opentelemetry_sdk_0_22::trace::TracerProvider::builder() 265 | .with_simple_exporter(exporter) 266 | .build(); 267 | 268 | let tracer = provider.versioned_tracer("reqwest", None::<&str>, None::<&str>, None); 269 | let _ = opentelemetry_0_22_pkg::global::set_tracer_provider(provider); 270 | opentelemetry_0_22_pkg::global::set_text_map_propagator( 271 | opentelemetry_sdk_0_22::propagation::TraceContextPropagator::new(), 272 | ); 273 | 274 | let telemetry = tracing_opentelemetry_0_23_pkg::layer().with_tracer(tracer); 275 | subscriber.with(telemetry) 276 | }; 277 | 278 | #[cfg(feature = "opentelemetry_0_23")] 279 | let subscriber = { 280 | use opentelemetry_0_23_pkg::trace::TracerProvider; 281 | use opentelemetry_stdout_0_4::SpanExporterBuilder; 282 | 283 | let exporter = SpanExporterBuilder::default() 284 | .with_writer(std::io::sink()) 285 | .build(); 286 | 287 | let provider = opentelemetry_sdk_0_23::trace::TracerProvider::builder() 288 | .with_simple_exporter(exporter) 289 | .build(); 290 | 291 | let tracer = provider.tracer_builder("reqwest").build(); 292 | let _ = opentelemetry_0_23_pkg::global::set_tracer_provider(provider); 293 | opentelemetry_0_23_pkg::global::set_text_map_propagator( 294 | opentelemetry_sdk_0_23::propagation::TraceContextPropagator::new(), 295 | ); 296 | 297 | let telemetry = tracing_opentelemetry_0_24_pkg::layer().with_tracer(tracer); 298 | subscriber.with(telemetry) 299 | }; 300 | 301 | #[cfg(feature = "opentelemetry_0_24")] 302 | let subscriber = { 303 | use opentelemetry_0_24_pkg::trace::TracerProvider; 304 | use opentelemetry_stdout_0_5::SpanExporterBuilder; 305 | 306 | let exporter = SpanExporterBuilder::default() 307 | .with_writer(std::io::sink()) 308 | .build(); 309 | 310 | let provider = opentelemetry_sdk_0_24::trace::TracerProvider::builder() 311 | .with_simple_exporter(exporter) 312 | .build(); 313 | 314 | let tracer = provider.tracer_builder("reqwest").build(); 315 | let _ = opentelemetry_0_24_pkg::global::set_tracer_provider(provider); 316 | opentelemetry_0_24_pkg::global::set_text_map_propagator( 317 | opentelemetry_sdk_0_24::propagation::TraceContextPropagator::new(), 318 | ); 319 | 320 | let telemetry = tracing_opentelemetry_0_25_pkg::layer().with_tracer(tracer); 321 | subscriber.with(telemetry) 322 | }; 323 | 324 | #[cfg(feature = "opentelemetry_0_25")] 325 | let subscriber = { 326 | use opentelemetry_0_25_pkg::trace::TracerProvider; 327 | 328 | let provider = opentelemetry_sdk_0_25::trace::TracerProvider::builder().build(); 329 | 330 | let tracer = provider.tracer_builder("reqwest").build(); 331 | let _ = opentelemetry_0_25_pkg::global::set_tracer_provider(provider); 332 | opentelemetry_0_25_pkg::global::set_text_map_propagator( 333 | opentelemetry_sdk_0_25::propagation::TraceContextPropagator::new(), 334 | ); 335 | 336 | let telemetry = tracing_opentelemetry_0_26_pkg::layer().with_tracer(tracer); 337 | subscriber.with(telemetry) 338 | }; 339 | 340 | #[cfg(feature = "opentelemetry_0_26")] 341 | let subscriber = { 342 | use opentelemetry_0_26_pkg::trace::TracerProvider; 343 | 344 | let provider = opentelemetry_sdk_0_26::trace::TracerProvider::builder().build(); 345 | 346 | let tracer = provider.tracer_builder("reqwest").build(); 347 | let _ = opentelemetry_0_26_pkg::global::set_tracer_provider(provider); 348 | opentelemetry_0_26_pkg::global::set_text_map_propagator( 349 | opentelemetry_sdk_0_26::propagation::TraceContextPropagator::new(), 350 | ); 351 | 352 | let telemetry = tracing_opentelemetry_0_27_pkg::layer().with_tracer(tracer); 353 | subscriber.with(telemetry) 354 | }; 355 | 356 | #[cfg(feature = "opentelemetry_0_27")] 357 | let subscriber = { 358 | use opentelemetry_0_27_pkg::trace::TracerProvider; 359 | 360 | let provider = opentelemetry_sdk_0_27::trace::TracerProvider::builder().build(); 361 | 362 | let tracer = provider.tracer("reqwest"); 363 | let _ = opentelemetry_0_27_pkg::global::set_tracer_provider(provider); 364 | opentelemetry_0_27_pkg::global::set_text_map_propagator( 365 | opentelemetry_sdk_0_27::propagation::TraceContextPropagator::new(), 366 | ); 367 | 368 | let telemetry = tracing_opentelemetry_0_28_pkg::layer().with_tracer(tracer); 369 | subscriber.with(telemetry) 370 | }; 371 | 372 | #[cfg(feature = "opentelemetry_0_28")] 373 | let subscriber = { 374 | use opentelemetry_0_28_pkg::trace::TracerProvider; 375 | 376 | let provider = opentelemetry_sdk_0_28::trace::SdkTracerProvider::builder().build(); 377 | 378 | let tracer = provider.tracer("reqwest"); 379 | let _ = opentelemetry_0_28_pkg::global::set_tracer_provider(provider); 380 | opentelemetry_0_28_pkg::global::set_text_map_propagator( 381 | opentelemetry_sdk_0_28::propagation::TraceContextPropagator::new(), 382 | ); 383 | 384 | let telemetry = tracing_opentelemetry_0_29_pkg::layer().with_tracer(tracer); 385 | subscriber.with(telemetry) 386 | }; 387 | 388 | #[cfg(feature = "opentelemetry_0_29")] 389 | let subscriber = { 390 | use opentelemetry_0_29_pkg::trace::TracerProvider; 391 | 392 | let provider = opentelemetry_sdk_0_29::trace::SdkTracerProvider::builder().build(); 393 | 394 | let tracer = provider.tracer("reqwest"); 395 | let _ = opentelemetry_0_29_pkg::global::set_tracer_provider(provider); 396 | opentelemetry_0_29_pkg::global::set_text_map_propagator( 397 | opentelemetry_sdk_0_29::propagation::TraceContextPropagator::new(), 398 | ); 399 | 400 | let telemetry = tracing_opentelemetry_0_30_pkg::layer().with_tracer(tracer); 401 | subscriber.with(telemetry) 402 | }; 403 | 404 | #[cfg(feature = "opentelemetry_0_30")] 405 | let subscriber = { 406 | use opentelemetry_0_30_pkg::trace::TracerProvider; 407 | 408 | let provider = opentelemetry_sdk_0_30::trace::SdkTracerProvider::builder().build(); 409 | 410 | let tracer = provider.tracer("reqwest"); 411 | let _ = opentelemetry_0_30_pkg::global::set_tracer_provider(provider); 412 | opentelemetry_0_30_pkg::global::set_text_map_propagator( 413 | opentelemetry_sdk_0_30::propagation::TraceContextPropagator::new(), 414 | ); 415 | 416 | let telemetry = tracing_opentelemetry_0_31_pkg::layer().with_tracer(tracer); 417 | subscriber.with(telemetry) 418 | }; 419 | 420 | tracing::subscriber::set_global_default(subscriber).unwrap(); 421 | }); 422 | 423 | // Mock server - sends all request headers back in the response 424 | let server = MockServer::start().await; 425 | Mock::given(any()) 426 | .respond_with(|req: &wiremock::Request| { 427 | req.headers 428 | .iter() 429 | .fold(ResponseTemplate::new(200), |resp, (k, v)| { 430 | resp.append_header(k.clone(), v.clone()) 431 | }) 432 | }) 433 | .mount(&server) 434 | .await; 435 | 436 | client 437 | .get(server.uri()) 438 | .send() 439 | .instrument(info_span!("some_span")) 440 | .await 441 | .unwrap() 442 | } 443 | 444 | #[tokio::test] 445 | async fn tracing_middleware_propagates_otel_data_even_when_the_span_is_disabled() { 446 | let client = ClientBuilder::new(reqwest::Client::new()) 447 | .with(TracingMiddleware::default()) 448 | .build(); 449 | 450 | let resp = make_echo_request_in_otel_context(client).await; 451 | 452 | assert!( 453 | resp.headers().contains_key("traceparent"), 454 | "by default, the tracing middleware will propagate otel contexts" 455 | ); 456 | } 457 | 458 | #[tokio::test] 459 | async fn context_no_propagated() { 460 | let client = ClientBuilder::new(reqwest::Client::new()) 461 | .with_init(Extension(DisableOtelPropagation)) 462 | .with(TracingMiddleware::default()) 463 | .build(); 464 | 465 | let resp = make_echo_request_in_otel_context(client).await; 466 | 467 | assert!( 468 | !resp.headers().contains_key("traceparent"), 469 | "request should not contain traceparent if context propagation is disabled" 470 | ); 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /reqwest-middleware/src/client.rs: -------------------------------------------------------------------------------- 1 | use http::Extensions; 2 | use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; 3 | use reqwest::{Body, Client, IntoUrl, Method, Request, Response}; 4 | use serde::Serialize; 5 | use std::convert::TryFrom; 6 | use std::fmt::{self, Display}; 7 | use std::sync::Arc; 8 | 9 | #[cfg(feature = "multipart")] 10 | use reqwest::multipart; 11 | 12 | use crate::error::Result; 13 | use crate::middleware::{Middleware, Next}; 14 | use crate::RequestInitialiser; 15 | 16 | /// A `ClientBuilder` is used to build a [`ClientWithMiddleware`]. 17 | /// 18 | /// [`ClientWithMiddleware`]: crate::ClientWithMiddleware 19 | pub struct ClientBuilder { 20 | client: Client, 21 | middleware_stack: Vec>, 22 | initialiser_stack: Vec>, 23 | } 24 | 25 | impl ClientBuilder { 26 | pub fn new(client: Client) -> Self { 27 | ClientBuilder { 28 | client, 29 | middleware_stack: Vec::new(), 30 | initialiser_stack: Vec::new(), 31 | } 32 | } 33 | 34 | /// This method allows creating a ClientBuilder 35 | /// from an existing ClientWithMiddleware instance 36 | pub fn from_client(client_with_middleware: ClientWithMiddleware) -> Self { 37 | Self { 38 | client: client_with_middleware.inner, 39 | middleware_stack: client_with_middleware.middleware_stack.into_vec(), 40 | initialiser_stack: client_with_middleware.initialiser_stack.into_vec(), 41 | } 42 | } 43 | 44 | /// Convenience method to attach middleware. 45 | /// 46 | /// If you need to keep a reference to the middleware after attaching, use [`with_arc`]. 47 | /// 48 | /// [`with_arc`]: Self::with_arc 49 | pub fn with(self, middleware: M) -> Self 50 | where 51 | M: Middleware, 52 | { 53 | self.with_arc(Arc::new(middleware)) 54 | } 55 | 56 | /// Add middleware to the chain. [`with`] is more ergonomic if you don't need the `Arc`. 57 | /// 58 | /// [`with`]: Self::with 59 | pub fn with_arc(mut self, middleware: Arc) -> Self { 60 | self.middleware_stack.push(middleware); 61 | self 62 | } 63 | 64 | /// Convenience method to attach a request initialiser. 65 | /// 66 | /// If you need to keep a reference to the initialiser after attaching, use [`with_arc_init`]. 67 | /// 68 | /// [`with_arc_init`]: Self::with_arc_init 69 | pub fn with_init(self, initialiser: I) -> Self 70 | where 71 | I: RequestInitialiser, 72 | { 73 | self.with_arc_init(Arc::new(initialiser)) 74 | } 75 | 76 | /// Add a request initialiser to the chain. [`with_init`] is more ergonomic if you don't need the `Arc`. 77 | /// 78 | /// [`with_init`]: Self::with_init 79 | pub fn with_arc_init(mut self, initialiser: Arc) -> Self { 80 | self.initialiser_stack.push(initialiser); 81 | self 82 | } 83 | 84 | /// Returns a `ClientWithMiddleware` using this builder configuration. 85 | pub fn build(self) -> ClientWithMiddleware { 86 | ClientWithMiddleware { 87 | inner: self.client, 88 | middleware_stack: self.middleware_stack.into_boxed_slice(), 89 | initialiser_stack: self.initialiser_stack.into_boxed_slice(), 90 | } 91 | } 92 | } 93 | 94 | /// `ClientWithMiddleware` is a wrapper around [`reqwest::Client`] which runs middleware on every 95 | /// request. 96 | #[derive(Clone, Default)] 97 | pub struct ClientWithMiddleware { 98 | inner: reqwest::Client, 99 | middleware_stack: Box<[Arc]>, 100 | initialiser_stack: Box<[Arc]>, 101 | } 102 | 103 | impl ClientWithMiddleware { 104 | /// See [`ClientBuilder`] for a more ergonomic way to build `ClientWithMiddleware` instances. 105 | pub fn new(client: Client, middleware_stack: T) -> Self 106 | where 107 | T: Into]>>, 108 | { 109 | ClientWithMiddleware { 110 | inner: client, 111 | middleware_stack: middleware_stack.into(), 112 | // TODO(conradludgate) - allow downstream code to control this manually if desired 113 | initialiser_stack: Box::new([]), 114 | } 115 | } 116 | 117 | /// Convenience method to make a `GET` request to a URL. 118 | /// 119 | /// # Errors 120 | /// 121 | /// This method fails whenever the supplied `Url` cannot be parsed. 122 | pub fn get(&self, url: U) -> RequestBuilder { 123 | self.request(Method::GET, url) 124 | } 125 | 126 | /// Convenience method to make a `POST` request to a URL. 127 | /// 128 | /// # Errors 129 | /// 130 | /// This method fails whenever the supplied `Url` cannot be parsed. 131 | pub fn post(&self, url: U) -> RequestBuilder { 132 | self.request(Method::POST, url) 133 | } 134 | 135 | /// Convenience method to make a `PUT` request to a URL. 136 | /// 137 | /// # Errors 138 | /// 139 | /// This method fails whenever the supplied `Url` cannot be parsed. 140 | pub fn put(&self, url: U) -> RequestBuilder { 141 | self.request(Method::PUT, url) 142 | } 143 | 144 | /// Convenience method to make a `PATCH` request to a URL. 145 | /// 146 | /// # Errors 147 | /// 148 | /// This method fails whenever the supplied `Url` cannot be parsed. 149 | pub fn patch(&self, url: U) -> RequestBuilder { 150 | self.request(Method::PATCH, url) 151 | } 152 | 153 | /// Convenience method to make a `DELETE` request to a URL. 154 | /// 155 | /// # Errors 156 | /// 157 | /// This method fails whenever the supplied `Url` cannot be parsed. 158 | pub fn delete(&self, url: U) -> RequestBuilder { 159 | self.request(Method::DELETE, url) 160 | } 161 | 162 | /// Convenience method to make a `HEAD` request to a URL. 163 | /// 164 | /// # Errors 165 | /// 166 | /// This method fails whenever the supplied `Url` cannot be parsed. 167 | pub fn head(&self, url: U) -> RequestBuilder { 168 | self.request(Method::HEAD, url) 169 | } 170 | 171 | /// Start building a `Request` with the `Method` and `Url`. 172 | /// 173 | /// Returns a `RequestBuilder`, which will allow setting headers and 174 | /// the request body before sending. 175 | /// 176 | /// # Errors 177 | /// 178 | /// This method fails whenever the supplied `Url` cannot be parsed. 179 | pub fn request(&self, method: Method, url: U) -> RequestBuilder { 180 | let req = RequestBuilder { 181 | inner: self.inner.request(method, url), 182 | extensions: Extensions::new(), 183 | middleware_stack: self.middleware_stack.clone(), 184 | initialiser_stack: self.initialiser_stack.clone(), 185 | }; 186 | self.initialiser_stack 187 | .iter() 188 | .fold(req, |req, i| i.init(req)) 189 | } 190 | 191 | /// Executes a `Request`. 192 | /// 193 | /// A `Request` can be built manually with `Request::new()` or obtained 194 | /// from a RequestBuilder with `RequestBuilder::build()`. 195 | /// 196 | /// You should prefer to use the `RequestBuilder` and 197 | /// `RequestBuilder::send()`. 198 | /// 199 | /// # Errors 200 | /// 201 | /// This method fails if there was an error while sending request, 202 | /// redirect loop was detected or redirect limit was exhausted. 203 | pub async fn execute(&self, req: Request) -> Result { 204 | let mut ext = Extensions::new(); 205 | self.execute_with_extensions(req, &mut ext).await 206 | } 207 | 208 | /// Executes a `Request` with initial [`Extensions`]. 209 | /// 210 | /// A `Request` can be built manually with `Request::new()` or obtained 211 | /// from a RequestBuilder with `RequestBuilder::build()`. 212 | /// 213 | /// You should prefer to use the `RequestBuilder` and 214 | /// `RequestBuilder::send()`. 215 | /// 216 | /// # Errors 217 | /// 218 | /// This method fails if there was an error while sending request, 219 | /// redirect loop was detected or redirect limit was exhausted. 220 | pub async fn execute_with_extensions( 221 | &self, 222 | req: Request, 223 | ext: &mut Extensions, 224 | ) -> Result { 225 | let next = Next::new(&self.inner, &self.middleware_stack); 226 | next.run(req, ext).await 227 | } 228 | } 229 | 230 | /// Create a `ClientWithMiddleware` without any middleware. 231 | impl From for ClientWithMiddleware { 232 | fn from(client: Client) -> Self { 233 | ClientWithMiddleware { 234 | inner: client, 235 | middleware_stack: Box::new([]), 236 | initialiser_stack: Box::new([]), 237 | } 238 | } 239 | } 240 | 241 | impl fmt::Debug for ClientWithMiddleware { 242 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 243 | // skipping middleware_stack field for now 244 | f.debug_struct("ClientWithMiddleware") 245 | .field("inner", &self.inner) 246 | .finish_non_exhaustive() 247 | } 248 | } 249 | 250 | #[cfg(not(target_arch = "wasm32"))] 251 | mod service { 252 | use std::{ 253 | future::Future, 254 | pin::Pin, 255 | task::{Context, Poll}, 256 | }; 257 | 258 | use crate::Result; 259 | use http::Extensions; 260 | use reqwest::{Request, Response}; 261 | 262 | use crate::{middleware::BoxFuture, ClientWithMiddleware, Next}; 263 | 264 | // this is meant to be semi-private, same as reqwest's pending 265 | pub struct Pending { 266 | inner: BoxFuture<'static, Result>, 267 | } 268 | 269 | impl Unpin for Pending {} 270 | 271 | impl Future for Pending { 272 | type Output = Result; 273 | 274 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 275 | self.inner.as_mut().poll(cx) 276 | } 277 | } 278 | 279 | impl tower_service::Service for ClientWithMiddleware { 280 | type Response = Response; 281 | type Error = crate::Error; 282 | type Future = Pending; 283 | 284 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 285 | self.inner.poll_ready(cx).map_err(crate::Error::Reqwest) 286 | } 287 | 288 | fn call(&mut self, req: Request) -> Self::Future { 289 | let inner = self.inner.clone(); 290 | let middlewares = self.middleware_stack.clone(); 291 | let mut extensions = Extensions::new(); 292 | Pending { 293 | inner: Box::pin(async move { 294 | let next = Next::new(&inner, &middlewares); 295 | next.run(req, &mut extensions).await 296 | }), 297 | } 298 | } 299 | } 300 | 301 | impl tower_service::Service for &'_ ClientWithMiddleware { 302 | type Response = Response; 303 | type Error = crate::Error; 304 | type Future = Pending; 305 | 306 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 307 | (&self.inner).poll_ready(cx).map_err(crate::Error::Reqwest) 308 | } 309 | 310 | fn call(&mut self, req: Request) -> Self::Future { 311 | let inner = self.inner.clone(); 312 | let middlewares = self.middleware_stack.clone(); 313 | let mut extensions = Extensions::new(); 314 | Pending { 315 | inner: Box::pin(async move { 316 | let next = Next::new(&inner, &middlewares); 317 | next.run(req, &mut extensions).await 318 | }), 319 | } 320 | } 321 | } 322 | } 323 | 324 | /// This is a wrapper around [`reqwest::RequestBuilder`] exposing the same API. 325 | #[must_use = "RequestBuilder does nothing until you 'send' it"] 326 | pub struct RequestBuilder { 327 | inner: reqwest::RequestBuilder, 328 | middleware_stack: Box<[Arc]>, 329 | initialiser_stack: Box<[Arc]>, 330 | extensions: Extensions, 331 | } 332 | 333 | impl RequestBuilder { 334 | /// Assemble a builder starting from an existing `Client` and a `Request`. 335 | pub fn from_parts(client: ClientWithMiddleware, request: Request) -> RequestBuilder { 336 | let inner = reqwest::RequestBuilder::from_parts(client.inner, request); 337 | RequestBuilder { 338 | inner, 339 | middleware_stack: client.middleware_stack, 340 | initialiser_stack: client.initialiser_stack, 341 | extensions: Extensions::new(), 342 | } 343 | } 344 | 345 | /// Add a `Header` to this Request. 346 | pub fn header(self, key: K, value: V) -> Self 347 | where 348 | HeaderName: TryFrom, 349 | >::Error: Into, 350 | HeaderValue: TryFrom, 351 | >::Error: Into, 352 | { 353 | RequestBuilder { 354 | inner: self.inner.header(key, value), 355 | ..self 356 | } 357 | } 358 | 359 | /// Add a set of Headers to the existing ones on this Request. 360 | /// 361 | /// The headers will be merged in to any already set. 362 | pub fn headers(self, headers: HeaderMap) -> Self { 363 | RequestBuilder { 364 | inner: self.inner.headers(headers), 365 | ..self 366 | } 367 | } 368 | 369 | #[cfg(not(target_arch = "wasm32"))] 370 | pub fn version(self, version: reqwest::Version) -> Self { 371 | RequestBuilder { 372 | inner: self.inner.version(version), 373 | ..self 374 | } 375 | } 376 | 377 | /// Enable HTTP basic authentication. 378 | /// 379 | /// ```rust 380 | /// # use anyhow::Error; 381 | /// 382 | /// # async fn run() -> Result<(), Error> { 383 | /// let client = reqwest_middleware::ClientWithMiddleware::from(reqwest::Client::new()); 384 | /// let resp = client.delete("http://httpbin.org/delete") 385 | /// .basic_auth("admin", Some("good password")) 386 | /// .send() 387 | /// .await?; 388 | /// # Ok(()) 389 | /// # } 390 | /// ``` 391 | pub fn basic_auth(self, username: U, password: Option

) -> Self 392 | where 393 | U: Display, 394 | P: Display, 395 | { 396 | RequestBuilder { 397 | inner: self.inner.basic_auth(username, password), 398 | ..self 399 | } 400 | } 401 | 402 | /// Enable HTTP bearer authentication. 403 | pub fn bearer_auth(self, token: T) -> Self 404 | where 405 | T: Display, 406 | { 407 | RequestBuilder { 408 | inner: self.inner.bearer_auth(token), 409 | ..self 410 | } 411 | } 412 | 413 | /// Set the request body. 414 | pub fn body>(self, body: T) -> Self { 415 | RequestBuilder { 416 | inner: self.inner.body(body), 417 | ..self 418 | } 419 | } 420 | 421 | /// Enables a request timeout. 422 | /// 423 | /// The timeout is applied from when the request starts connecting until the 424 | /// response body has finished. It affects only this request and overrides 425 | /// the timeout configured using `ClientBuilder::timeout()`. 426 | #[cfg(not(target_arch = "wasm32"))] 427 | pub fn timeout(self, timeout: std::time::Duration) -> Self { 428 | RequestBuilder { 429 | inner: self.inner.timeout(timeout), 430 | ..self 431 | } 432 | } 433 | 434 | #[cfg(feature = "multipart")] 435 | #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] 436 | pub fn multipart(self, multipart: multipart::Form) -> Self { 437 | RequestBuilder { 438 | inner: self.inner.multipart(multipart), 439 | ..self 440 | } 441 | } 442 | 443 | /// Modify the query string of the URL. 444 | /// 445 | /// Modifies the URL of this request, adding the parameters provided. 446 | /// This method appends and does not overwrite. This means that it can 447 | /// be called multiple times and that existing query parameters are not 448 | /// overwritten if the same key is used. The key will simply show up 449 | /// twice in the query string. 450 | /// Calling `.query(&[("foo", "a"), ("foo", "b")])` gives `"foo=a&foo=b"`. 451 | /// 452 | /// # Note 453 | /// This method does not support serializing a single key-value 454 | /// pair. Instead of using `.query(("key", "val"))`, use a sequence, such 455 | /// as `.query(&[("key", "val")])`. It's also possible to serialize structs 456 | /// and maps into a key-value pair. 457 | /// 458 | /// # Errors 459 | /// This method will fail if the object you provide cannot be serialized 460 | /// into a query string. 461 | pub fn query(self, query: &T) -> Self { 462 | RequestBuilder { 463 | inner: self.inner.query(query), 464 | ..self 465 | } 466 | } 467 | 468 | /// Send a form body. 469 | /// 470 | /// Sets the body to the url encoded serialization of the passed value, 471 | /// and also sets the `Content-Type: application/x-www-form-urlencoded` 472 | /// header. 473 | /// 474 | /// ```rust 475 | /// # use anyhow::Error; 476 | /// # use std::collections::HashMap; 477 | /// # 478 | /// # async fn run() -> Result<(), Error> { 479 | /// let mut params = HashMap::new(); 480 | /// params.insert("lang", "rust"); 481 | /// 482 | /// let client = reqwest_middleware::ClientWithMiddleware::from(reqwest::Client::new()); 483 | /// let res = client.post("http://httpbin.org") 484 | /// .form(¶ms) 485 | /// .send() 486 | /// .await?; 487 | /// # Ok(()) 488 | /// # } 489 | /// ``` 490 | /// 491 | /// # Errors 492 | /// 493 | /// This method fails if the passed value cannot be serialized into 494 | /// url encoded format 495 | pub fn form(self, form: &T) -> Self { 496 | RequestBuilder { 497 | inner: self.inner.form(form), 498 | ..self 499 | } 500 | } 501 | 502 | /// Send a JSON body. 503 | /// 504 | /// # Optional 505 | /// 506 | /// This requires the optional `json` feature enabled. 507 | /// 508 | /// # Errors 509 | /// 510 | /// Serialization can fail if `T`'s implementation of `Serialize` decides to 511 | /// fail, or if `T` contains a map with non-string keys. 512 | #[cfg(feature = "json")] 513 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 514 | pub fn json(self, json: &T) -> Self { 515 | RequestBuilder { 516 | inner: self.inner.json(json), 517 | ..self 518 | } 519 | } 520 | 521 | /// Disable CORS on fetching the request. 522 | /// 523 | /// # WASM 524 | /// 525 | /// This option is only effective with WebAssembly target. 526 | /// 527 | /// The [request mode][mdn] will be set to 'no-cors'. 528 | /// 529 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode 530 | #[deprecated(note = "Deprecated Upstream")] 531 | pub fn fetch_mode_no_cors(self) -> Self { 532 | RequestBuilder { 533 | inner: self.inner.fetch_mode_no_cors(), 534 | ..self 535 | } 536 | } 537 | 538 | /// Build a `Request`, which can be inspected, modified and executed with 539 | /// `ClientWithMiddleware::execute()`. 540 | pub fn build(self) -> reqwest::Result { 541 | self.inner.build() 542 | } 543 | 544 | /// Build a `Request`, which can be inspected, modified and executed with 545 | /// `ClientWithMiddleware::execute()`. 546 | /// 547 | /// This is similar to [`RequestBuilder::build()`], but also returns the 548 | /// embedded `Client`. 549 | pub fn build_split(self) -> (ClientWithMiddleware, reqwest::Result) { 550 | let Self { 551 | inner, 552 | middleware_stack, 553 | initialiser_stack, 554 | .. 555 | } = self; 556 | let (inner, req) = inner.build_split(); 557 | let client = ClientWithMiddleware { 558 | inner, 559 | middleware_stack, 560 | initialiser_stack, 561 | }; 562 | (client, req) 563 | } 564 | 565 | /// Inserts the extension into this request builder 566 | pub fn with_extension(mut self, extension: T) -> Self { 567 | self.extensions.insert(extension); 568 | self 569 | } 570 | 571 | /// Returns a mutable reference to the internal set of extensions for this request 572 | pub fn extensions(&mut self) -> &mut Extensions { 573 | &mut self.extensions 574 | } 575 | 576 | /// Constructs the Request and sends it to the target URL, returning a 577 | /// future Response. 578 | /// 579 | /// # Errors 580 | /// 581 | /// This method fails if there was an error while sending request, 582 | /// redirect loop was detected or redirect limit was exhausted. 583 | /// 584 | /// # Example 585 | /// 586 | /// ```no_run 587 | /// # use anyhow::Error; 588 | /// # 589 | /// # async fn run() -> Result<(), Error> { 590 | /// let response = reqwest_middleware::ClientWithMiddleware::from(reqwest::Client::new()) 591 | /// .get("https://hyper.rs") 592 | /// .send() 593 | /// .await?; 594 | /// # Ok(()) 595 | /// # } 596 | /// ``` 597 | pub async fn send(mut self) -> Result { 598 | let mut extensions = std::mem::take(self.extensions()); 599 | let (client, req) = self.build_split(); 600 | client.execute_with_extensions(req?, &mut extensions).await 601 | } 602 | 603 | /// Attempt to clone the RequestBuilder. 604 | /// 605 | /// `None` is returned if the RequestBuilder can not be cloned, 606 | /// i.e. if the request body is a stream. 607 | /// 608 | /// # Examples 609 | /// 610 | /// ``` 611 | /// # use reqwest::Error; 612 | /// # 613 | /// # fn run() -> Result<(), Error> { 614 | /// let client = reqwest_middleware::ClientWithMiddleware::from(reqwest::Client::new()); 615 | /// let builder = client.post("http://httpbin.org/post") 616 | /// .body("from a &str!"); 617 | /// let clone = builder.try_clone(); 618 | /// assert!(clone.is_some()); 619 | /// # Ok(()) 620 | /// # } 621 | /// ``` 622 | pub fn try_clone(&self) -> Option { 623 | self.inner.try_clone().map(|inner| RequestBuilder { 624 | inner, 625 | middleware_stack: self.middleware_stack.clone(), 626 | initialiser_stack: self.initialiser_stack.clone(), 627 | extensions: self.extensions.clone(), 628 | }) 629 | } 630 | } 631 | 632 | impl fmt::Debug for RequestBuilder { 633 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 634 | // skipping middleware_stack field for now 635 | f.debug_struct("RequestBuilder") 636 | .field("inner", &self.inner) 637 | .finish_non_exhaustive() 638 | } 639 | } 640 | --------------------------------------------------------------------------------