Skip to main content

better_fetch/tower/
stack.rs

1//! Helpers for building transport stacks with [`ServiceBuilder`](tower::ServiceBuilder).
2
3use crate::backend::{HttpRequest, HttpResponse, HttpStreamingResponse};
4use crate::Error;
5
6use super::{
7    BoxHttpService, BoxStreamingHttpService, ReqwestHttpService, ReqwestStreamingHttpService,
8};
9
10pub use tower::limit::{ConcurrencyLimitLayer, RateLimitLayer};
11pub use tower::timeout::TimeoutLayer;
12pub use tower::ServiceBuilder;
13
14/// Creates a reqwest-backed inner service for layer stacking.
15pub fn reqwest_service(client: reqwest::Client) -> ReqwestHttpService {
16    ReqwestHttpService::new(client)
17}
18
19/// Builds a type-erased transport stack from a reqwest client and a configuration closure.
20///
21/// Pass the result to [`ClientBuilder::http_service_boxed`](crate::ClientBuilder::http_service_boxed)
22/// or use [`ClientBuilder::transport_stack`](crate::ClientBuilder::transport_stack).
23pub fn build<F>(client: reqwest::Client, configure: F) -> BoxHttpService
24where
25    F: FnOnce(ReqwestHttpService) -> BoxHttpService,
26{
27    configure(ReqwestHttpService::new(client))
28}
29
30/// Builds buffered and streaming transport stacks from one configuration closure.
31pub fn build_dual<F>(
32    client: reqwest::Client,
33    configure: F,
34) -> (BoxHttpService, BoxStreamingHttpService)
35where
36    F: FnOnce(
37        ReqwestHttpService,
38        ReqwestStreamingHttpService,
39    ) -> (BoxHttpService, BoxStreamingHttpService),
40{
41    configure(
42        ReqwestHttpService::new(client.clone()),
43        ReqwestStreamingHttpService::new(client),
44    )
45}
46
47/// Extension trait to box a configured service stack.
48pub trait IntoBoxHttpService: Sized {
49    /// Boxes `self` as [`BoxHttpService`].
50    fn into_box(self) -> BoxHttpService;
51}
52
53impl<S> IntoBoxHttpService for S
54where
55    S: tower::Service<HttpRequest, Response = HttpResponse, Error = Error> + Clone + Send + 'static,
56    S::Future: Send + 'static,
57{
58    fn into_box(self) -> BoxHttpService {
59        BoxHttpService::new(self)
60    }
61}
62
63/// Extension trait to box a configured streaming service stack.
64pub trait IntoBoxStreamingHttpService: Sized {
65    /// Boxes `self` as [`BoxStreamingHttpService`].
66    fn into_streaming_box(self) -> BoxStreamingHttpService;
67}
68
69impl<S> IntoBoxStreamingHttpService for S
70where
71    S: tower::Service<HttpRequest, Response = HttpStreamingResponse, Error = Error>
72        + Clone
73        + Send
74        + 'static,
75    S::Future: Send + 'static,
76{
77    fn into_streaming_box(self) -> BoxStreamingHttpService {
78        BoxStreamingHttpService::new(self)
79    }
80}
81
82/// Convenience: concurrency limit on the transport stack.
83pub fn with_concurrency_limit(client: reqwest::Client, max_in_flight: usize) -> BoxHttpService {
84    build(client, |inner| {
85        ServiceBuilder::new()
86            .layer(ConcurrencyLimitLayer::new(max_in_flight))
87            .service(inner)
88            .into_box()
89    })
90}
91
92/// Wraps the reqwest inner service with [`tower::buffer::Buffer`](https://docs.rs/tower/latest/tower/buffer/struct.Buffer.html).
93///
94/// Use when the inner service is not [`Clone`] or cloning it is expensive. `Buffer::new`
95/// spawns a worker on the Tokio runtime; lightweight `Buffer` clones enqueue work to that
96/// worker. This is optional for typical reqwest-backed stacks — [`ServiceBackend`](crate::tower::ServiceBackend)
97/// already clones the boxed stack per request.
98pub fn with_buffer(client: reqwest::Client, capacity: usize) -> BoxHttpService {
99    build(client, |inner| {
100        let buffered = tower::buffer::Buffer::new(inner, capacity);
101        ServiceBuilder::new()
102            .map_err(|e: tower::BoxError| Error::transport_message(e.to_string()))
103            .service(buffered)
104            .into_box()
105    })
106}
107
108/// Logs each transport call at `DEBUG` (wire-level; complements [`LoggerPlugin`](crate::LoggerPlugin)).
109pub fn with_request_logging(client: reqwest::Client) -> BoxHttpService {
110    build(client, |inner| {
111        ServiceBuilder::new()
112            .map_request(|req: HttpRequest| {
113                tracing::debug!(method = %req.method, url = %req.url, "better-fetch transport");
114                req
115            })
116            .service(inner)
117            .into_box()
118    })
119}