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.
83///
84/// When combining this with [`ClientBuilder::max_in_flight`](crate::ClientBuilder::max_in_flight),
85/// call [`ClientBuilder::wire_concurrency_limit`](crate::ClientBuilder::wire_concurrency_limit)
86/// with the same `max_in_flight` value so `build()` can warn if both caps use identical limits.
87pub fn with_concurrency_limit(client: reqwest::Client, max_in_flight: usize) -> BoxHttpService {
88    build(client, |inner| {
89        ServiceBuilder::new()
90            .layer(ConcurrencyLimitLayer::new(max_in_flight))
91            .service(inner)
92            .into_box()
93    })
94}
95
96/// Wraps the reqwest inner service with [`tower::buffer::Buffer`](https://docs.rs/tower/latest/tower/buffer/struct.Buffer.html).
97///
98/// Use when the inner service is not [`Clone`] or cloning it is expensive. `Buffer::new`
99/// spawns a worker on the Tokio runtime; lightweight `Buffer` clones enqueue work to that
100/// worker. This is optional for typical reqwest-backed stacks — [`ServiceBackend`](crate::tower::ServiceBackend)
101/// already clones the boxed stack per request.
102pub fn with_buffer(client: reqwest::Client, capacity: usize) -> BoxHttpService {
103    build(client, |inner| {
104        let buffered = tower::buffer::Buffer::new(inner, capacity);
105        ServiceBuilder::new()
106            .map_err(|e: tower::BoxError| Error::transport_message(e.to_string()))
107            .service(buffered)
108            .into_box()
109    })
110}
111
112/// Logs each transport call at `DEBUG` (wire-level; complements [`LoggerPlugin`](crate::LoggerPlugin)).
113pub fn with_request_logging(client: reqwest::Client) -> BoxHttpService {
114    build(client, |inner| {
115        ServiceBuilder::new()
116            .map_request(|req: HttpRequest| {
117                tracing::debug!(method = %req.method, url = %req.url, "better-fetch transport");
118                req
119            })
120            .service(inner)
121            .into_box()
122    })
123}