Skip to main content

rama_http/service/client/
blocking.rs

1//! Blocking HTTP client boundary.
2//!
3//! [`Client::try_new`] creates and owns a dedicated runtime thread. Cloning the
4//! client shares both the asynchronous service and that runtime.
5//!
6//! # Panics
7//!
8//! Blocking client operations must not run directly on an asynchronous
9//! executor thread.
10
11use super::ext::{
12    BlockingRequestBuilder, RequestBuilder, RequestBuilderState, request_builder_mode,
13    request_builder_state,
14};
15use crate::{Body as HttpBody, Method, Request, Response as HttpResponse, StreamingBody};
16use rama_core::{
17    BlockingService,
18    bytes::Bytes,
19    error::{BoxError, ErrorContext as _, ErrorExt as _},
20    extensions::Extensions,
21    futures::TryStreamExt as _,
22    rt::blocking::{Guarded, Io as BlockingIo, Runtime},
23    stream::io::StreamReader,
24};
25use std::{fmt, io, pin::Pin, sync::Arc, time::Duration};
26use tokio::io::AsyncRead;
27
28use super::ext::IntoUrl;
29
30/// A cloneable blocking boundary around an asynchronous HTTP service.
31pub struct Client<S> {
32    service: rama_core::rt::blocking::Service<Arc<S>>,
33}
34
35impl<S> Client<S> {
36    /// Create a client with its own dedicated runtime thread.
37    pub fn try_new(service: S) -> io::Result<Self> {
38        let runtime = Runtime::builder()
39            .with_thread_name("rama-http-blocking-runtime")
40            .try_build()?;
41        Ok(Self::with_runtime(service, &runtime))
42    }
43
44    /// Create a client using an explicitly supplied runtime.
45    #[must_use]
46    pub fn with_runtime(service: S, runtime: &Runtime) -> Self {
47        Self {
48            service: runtime.service(Arc::new(service)),
49        }
50    }
51
52    /// Borrow the asynchronous HTTP service.
53    #[must_use]
54    pub fn get_ref(&self) -> &S {
55        self.service.get_ref().as_ref()
56    }
57
58    /// Clone the shared asynchronous HTTP service.
59    #[must_use]
60    pub fn clone_service(&self) -> Arc<S> {
61        Arc::clone(self.service.get_ref())
62    }
63
64    /// Borrow the blocking runtime.
65    #[must_use]
66    pub fn runtime(&self) -> &Runtime {
67        self.service.runtime()
68    }
69
70    /// Start building a `GET` request.
71    pub fn get<B>(&self, url: impl IntoUrl) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
72    where
73        S: rama_core::Service<Request, Output = HttpResponse<B>>,
74    {
75        self.request(Method::GET, url)
76    }
77
78    /// Start building a `POST` request.
79    pub fn post<B>(&self, url: impl IntoUrl) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
80    where
81        S: rama_core::Service<Request, Output = HttpResponse<B>>,
82    {
83        self.request(Method::POST, url)
84    }
85
86    /// Start building a `PUT` request.
87    pub fn put<B>(&self, url: impl IntoUrl) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
88    where
89        S: rama_core::Service<Request, Output = HttpResponse<B>>,
90    {
91        self.request(Method::PUT, url)
92    }
93
94    /// Start building a `PATCH` request.
95    pub fn patch<B>(&self, url: impl IntoUrl) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
96    where
97        S: rama_core::Service<Request, Output = HttpResponse<B>>,
98    {
99        self.request(Method::PATCH, url)
100    }
101
102    /// Start building a `DELETE` request.
103    pub fn delete<B>(&self, url: impl IntoUrl) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
104    where
105        S: rama_core::Service<Request, Output = HttpResponse<B>>,
106    {
107        self.request(Method::DELETE, url)
108    }
109
110    /// Start building a `HEAD` request.
111    pub fn head<B>(&self, url: impl IntoUrl) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
112    where
113        S: rama_core::Service<Request, Output = HttpResponse<B>>,
114    {
115        self.request(Method::HEAD, url)
116    }
117
118    /// Start building a `CONNECT` request.
119    pub fn connect<B>(&self, url: impl IntoUrl) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
120    where
121        S: rama_core::Service<Request, Output = HttpResponse<B>>,
122    {
123        self.request(Method::CONNECT, url)
124    }
125
126    /// Start building a request.
127    pub fn request<B>(
128        &self,
129        method: Method,
130        url: impl IntoUrl,
131    ) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
132    where
133        S: rama_core::Service<Request, Output = HttpResponse<B>>,
134    {
135        RequestBuilder::from_state(self, request_builder_state(method, url))
136    }
137
138    /// Start with an existing request.
139    pub fn build_from_request<B, RequestBody>(
140        &self,
141        request: Request<RequestBody>,
142    ) -> BlockingRequestBuilder<'_, Self, HttpResponse<B>>
143    where
144        S: rama_core::Service<Request, Output = HttpResponse<B>>,
145        RequestBody: Into<HttpBody>,
146    {
147        RequestBuilder::from_state(self, RequestBuilderState::PostBody(request.map(Into::into)))
148    }
149
150    /// Execute a request, blocking until response headers are available.
151    pub fn execute<B>(&self, request: Request) -> Result<Response, BoxError>
152    where
153        S: rama_core::Service<Request, Output = HttpResponse<B>, Error: Into<BoxError>>,
154        B: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + 'static,
155    {
156        let uri = request.uri().clone();
157        let response = self.service.serve(request).map_err(|err| {
158            let err: BoxError = err.into();
159            err.context(uri)
160        })?;
161        Ok(Response::from_guarded(response))
162    }
163
164    fn execute_with_timeout<B>(
165        &self,
166        request: Request,
167        timeout: Duration,
168    ) -> Result<Response, BoxError>
169    where
170        S: rama_core::Service<Request, Output = HttpResponse<B>, Error: Into<BoxError>>,
171        B: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + 'static,
172    {
173        let uri = request.uri().clone();
174        let service = Arc::clone(self.service.get_ref());
175        let result = self.service.runtime().block_on_task(async move {
176            tokio::time::timeout(timeout, service.serve(request)).await
177        });
178
179        let response = match result {
180            Ok(Ok(response)) => response,
181            Ok(Err(err)) => {
182                let err: BoxError = err.into();
183                return Err(err.context(uri));
184            }
185            Err(err) => {
186                return Err(err
187                    .context("roundtrip timeout reached")
188                    .context_debug_field("timeout", timeout));
189            }
190        };
191
192        Ok(Response::from_guarded(Guarded::new(
193            response,
194            self.service.runtime().clone(),
195        )))
196    }
197}
198
199impl<S> Clone for Client<S> {
200    fn clone(&self) -> Self {
201        Self {
202            service: self.service.clone(),
203        }
204    }
205}
206
207impl<S> fmt::Debug for Client<S>
208where
209    S: fmt::Debug,
210{
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        f.debug_struct("Client")
213            .field("service", self.get_ref())
214            .field("runtime", self.runtime())
215            .finish()
216    }
217}
218
219impl<S, B> BlockingService<Request> for Client<S>
220where
221    S: rama_core::Service<Request, Output = HttpResponse<B>, Error: Into<BoxError>>,
222    B: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + 'static,
223{
224    type Output = Response;
225    type Error = BoxError;
226
227    fn serve(&self, input: Request) -> Result<Self::Output, Self::Error> {
228        self.execute(input)
229    }
230}
231
232impl<S, B> RequestBuilder<'_, Client<S>, HttpResponse<B>, request_builder_mode::Blocking>
233where
234    S: rama_core::Service<Request, Output = HttpResponse<B>, Error: Into<BoxError>>,
235    B: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + 'static,
236{
237    /// Construct the request without sending it.
238    pub fn try_into_request(self) -> Result<Request, BoxError> {
239        self.build()
240    }
241
242    /// Construct and send the request, blocking until response headers arrive.
243    pub fn send(self) -> Result<Response, BoxError> {
244        let client = self.http_client_service;
245        client.execute(self.build()?)
246    }
247
248    /// Construct and send the request with a response-header timeout.
249    ///
250    /// The timeout does not include reading the response body.
251    pub fn send_with_timeout(self, timeout: Duration) -> Result<Response, BoxError> {
252        let client = self.http_client_service;
253        client.execute_with_timeout(self.build()?, timeout)
254    }
255}
256
257type AsyncBodyReader = Pin<Box<dyn AsyncRead + Send + 'static>>;
258
259/// A blocking HTTP response body.
260pub struct Body {
261    inner: BlockingIo<AsyncBodyReader>,
262}
263
264impl Body {
265    fn new<B>(body: B, runtime: &Runtime) -> Self
266    where
267        B: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + 'static,
268    {
269        let stream = crate::body::util::BodyDataStream::new(body)
270            .map_err(|err| io::Error::other(err.into()));
271        let reader: AsyncBodyReader = Box::pin(StreamReader::new(stream));
272        Self {
273            inner: runtime.io(reader),
274        }
275    }
276
277    /// Borrow the runtime keeping this body live.
278    #[must_use]
279    pub fn runtime(&self) -> &Runtime {
280        self.inner.runtime()
281    }
282}
283
284impl fmt::Debug for Body {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        f.debug_struct("Body").finish_non_exhaustive()
287    }
288}
289
290impl io::Read for Body {
291    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
292        self.inner.read(buf)
293    }
294}
295
296/// An HTTP response with a blocking body reader and an attached runtime lease.
297#[derive(Debug)]
298pub struct Response {
299    parts: crate::response::Parts,
300    body: Body,
301}
302
303impl Response {
304    fn from_guarded<B>(response: Guarded<HttpResponse<B>>) -> Self
305    where
306        B: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + 'static,
307    {
308        let (response, runtime) = response.into_parts();
309        let (parts, body) = response.into_parts();
310        Self {
311            parts,
312            body: Body::new(body, &runtime),
313        }
314    }
315
316    /// Return the HTTP status.
317    #[must_use]
318    pub fn status(&self) -> crate::StatusCode {
319        self.parts.status
320    }
321
322    /// Return the HTTP version.
323    #[must_use]
324    pub fn version(&self) -> crate::Version {
325        self.parts.version
326    }
327
328    /// Borrow the response headers.
329    #[must_use]
330    pub fn headers(&self) -> &crate::HeaderMap {
331        &self.parts.headers
332    }
333
334    /// Mutably borrow the response headers.
335    pub fn headers_mut(&mut self) -> &mut crate::HeaderMap {
336        &mut self.parts.headers
337    }
338
339    /// Borrow the response extensions.
340    #[must_use]
341    pub fn extensions(&self) -> &Extensions {
342        &self.parts.extensions
343    }
344
345    /// Mutably borrow the response extensions.
346    pub fn extensions_mut(&mut self) -> &mut Extensions {
347        &mut self.parts.extensions
348    }
349
350    /// Borrow the blocking response body.
351    #[must_use]
352    pub fn body(&self) -> &Body {
353        &self.body
354    }
355
356    /// Mutably borrow the blocking response body.
357    pub fn body_mut(&mut self) -> &mut Body {
358        &mut self.body
359    }
360
361    /// Consume the response and return its body.
362    #[must_use]
363    pub fn into_body(self) -> Body {
364        self.body
365    }
366
367    /// Consume the response and return its head and body.
368    #[must_use]
369    pub fn into_parts(self) -> (crate::response::Parts, Body) {
370        (self.parts, self.body)
371    }
372
373    /// Read the complete body as bytes.
374    pub fn try_into_bytes(mut self) -> Result<Bytes, BoxError> {
375        use io::Read as _;
376
377        let mut bytes = Vec::new();
378        self.read_to_end(&mut bytes)
379            .context("read HTTP response body")?;
380        Ok(bytes.into())
381    }
382
383    /// Read the complete body as UTF-8 text.
384    pub fn try_into_string(mut self) -> Result<String, BoxError> {
385        use io::Read as _;
386
387        let mut text = String::new();
388        self.read_to_string(&mut text)
389            .context("read HTTP response body as UTF-8")?;
390        Ok(text)
391    }
392
393    /// Buffer and deserialize the complete body as JSON.
394    pub fn try_into_json<T>(self) -> Result<T, BoxError>
395    where
396        T: serde::de::DeserializeOwned,
397    {
398        let bytes = self.try_into_bytes()?;
399        serde_json::from_slice(bytes.as_ref()).context("deserialize HTTP response body as JSON")
400    }
401
402    /// Deserialize JSON directly from the streaming body reader.
403    pub fn try_into_json_streaming<T>(mut self) -> Result<T, BoxError>
404    where
405        T: serde::de::DeserializeOwned,
406    {
407        deserialize_json_streaming(&mut self)
408    }
409
410    /// Copy the response body into a blocking writer.
411    pub fn copy_to<W>(&mut self, writer: &mut W) -> Result<u64, BoxError>
412    where
413        W: io::Write + ?Sized,
414    {
415        io::copy(self, writer).context("copy HTTP response body")
416    }
417}
418
419fn deserialize_json_streaming<T>(reader: impl io::Read) -> Result<T, BoxError>
420where
421    T: serde::de::DeserializeOwned,
422{
423    serde_json::from_reader(io::BufReader::new(reader))
424        .context("streaming-deserialize HTTP response body as JSON")
425}
426
427impl io::Read for Response {
428    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
429        self.body.read(buf)
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use rama_core::{error::BoxError, service::service_fn};
437    use rama_http_types::BodyExtractExt as _;
438    use std::sync::atomic::{AtomicUsize, Ordering};
439
440    #[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
441    struct Message {
442        value: String,
443    }
444
445    #[test]
446    fn client_owns_runtime_is_cloneable_and_reuses_request_builder() {
447        let client = Client::try_new(service_fn(|request: Request| async move {
448            let payload: Message = request.try_into_json().await?;
449            let body = serde_json::to_vec(&payload)?;
450            Ok::<_, BoxError>(HttpResponse::new(HttpBody::from(body)))
451        }))
452        .unwrap();
453        let cloned = client.clone();
454        drop(client);
455
456        let response = cloned
457            .post("https://example.test/messages")
458            .header("x-rama", "blocking")
459            .json(&Message {
460                value: "hello".to_owned(),
461            })
462            .send()
463            .unwrap();
464
465        assert_eq!(response.status(), crate::StatusCode::OK);
466        assert_eq!(
467            response.try_into_json::<Message>().unwrap(),
468            Message {
469                value: "hello".to_owned()
470            }
471        );
472    }
473
474    #[test]
475    fn response_implements_read() {
476        let client = Client::try_new(service_fn(|_: Request| async {
477            Ok::<_, BoxError>(HttpResponse::new(HttpBody::from("hello")))
478        }))
479        .unwrap();
480
481        let mut response = client.get("http://example.test").send().unwrap();
482        let mut text = String::new();
483        io::Read::read_to_string(&mut response, &mut text).unwrap();
484        assert_eq!(text, "hello");
485    }
486
487    #[test]
488    fn client_request_works_from_tokio_spawn_blocking() {
489        let client = Client::try_new(service_fn(|_: Request| async {
490            Ok::<_, BoxError>(HttpResponse::new(HttpBody::from("hello")))
491        }))
492        .unwrap();
493        let outer = tokio::runtime::Builder::new_multi_thread()
494            .worker_threads(1)
495            .enable_all()
496            .build()
497            .unwrap();
498
499        let text = outer.block_on(async move {
500            tokio::task::spawn_blocking(move || {
501                client
502                    .get("http://example.test")
503                    .send()
504                    .unwrap()
505                    .try_into_string()
506                    .unwrap()
507            })
508            .await
509            .unwrap()
510        });
511        assert_eq!(text, "hello");
512    }
513
514    #[test]
515    fn response_body_keeps_runtime_alive_after_client_drop() {
516        let client = Client::try_new(service_fn(|_: Request| async {
517            let body = HttpBody::from_stream(rama_core::futures::stream::once(async {
518                tokio::time::sleep(Duration::from_millis(10)).await;
519                Ok::<_, core::convert::Infallible>(Bytes::from_static(b"still alive"))
520            }));
521            Ok::<_, BoxError>(HttpResponse::new(body))
522        }))
523        .unwrap();
524
525        let response = client.get("http://example.test").send().unwrap();
526        drop(client);
527        assert_eq!(response.try_into_string().unwrap(), "still alive");
528    }
529
530    #[test]
531    fn streaming_json_buffers_source_reads() {
532        struct CountingReader {
533            inner: io::Cursor<Vec<u8>>,
534            reads: Arc<AtomicUsize>,
535        }
536
537        impl io::Read for CountingReader {
538            fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
539                self.reads.fetch_add(1, Ordering::Relaxed);
540                self.inner.read(buf)
541            }
542        }
543
544        let expected = Message {
545            value: "x".repeat(64 * 1024),
546        };
547        let reads = Arc::new(AtomicUsize::new(0));
548        let reader = CountingReader {
549            inner: io::Cursor::new(serde_json::to_vec(&expected).unwrap()),
550            reads: reads.clone(),
551        };
552
553        assert_eq!(
554            deserialize_json_streaming::<Message>(reader).unwrap(),
555            expected
556        );
557        assert!(
558            reads.load(Ordering::Relaxed) < 32,
559            "streaming JSON should read in buffered chunks"
560        );
561    }
562
563    #[cfg(feature = "dial9")]
564    #[test]
565    fn client_request_runs_inside_dial9_session() {
566        let temp_dir = tempfile::tempdir().unwrap();
567        let config = rama_core::telemetry::dial9::Dial9Config::builder()
568            .enabled(true)
569            .base_path(temp_dir.path().join("blocking-http.bin"))
570            .max_file_size(1024 * 1024)
571            .max_total_size(4 * 1024 * 1024)
572            .build()
573            .unwrap();
574        let runtime = Runtime::builder()
575            .with_dial9_config(config)
576            .try_build()
577            .unwrap();
578        let client = Client::with_runtime(
579            service_fn(|_: Request| async {
580                assert!(
581                    rama_core::telemetry::dial9::telemetry::TelemetryHandle::current().is_enabled()
582                );
583                Ok::<_, BoxError>(HttpResponse::new(HttpBody::from("tracked")))
584            }),
585            &runtime,
586        );
587
588        assert_eq!(
589            client
590                .get("https://example.test")
591                .send()
592                .unwrap()
593                .try_into_string()
594                .unwrap(),
595            "tracked"
596        );
597        assert_eq!(
598            client
599                .get("https://example.test/timeout")
600                .send_with_timeout(Duration::from_secs(1))
601                .unwrap()
602                .try_into_string()
603                .unwrap(),
604            "tracked"
605        );
606    }
607}