1use 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
30pub struct Client<S> {
32 service: rama_core::rt::blocking::Service<Arc<S>>,
33}
34
35impl<S> Client<S> {
36 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 #[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 #[must_use]
54 pub fn get_ref(&self) -> &S {
55 self.service.get_ref().as_ref()
56 }
57
58 #[must_use]
60 pub fn clone_service(&self) -> Arc<S> {
61 Arc::clone(self.service.get_ref())
62 }
63
64 #[must_use]
66 pub fn runtime(&self) -> &Runtime {
67 self.service.runtime()
68 }
69
70 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 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 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 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 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 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 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 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 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 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 pub fn try_into_request(self) -> Result<Request, BoxError> {
239 self.build()
240 }
241
242 pub fn send(self) -> Result<Response, BoxError> {
244 let client = self.http_client_service;
245 client.execute(self.build()?)
246 }
247
248 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
259pub 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 #[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#[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 #[must_use]
318 pub fn status(&self) -> crate::StatusCode {
319 self.parts.status
320 }
321
322 #[must_use]
324 pub fn version(&self) -> crate::Version {
325 self.parts.version
326 }
327
328 #[must_use]
330 pub fn headers(&self) -> &crate::HeaderMap {
331 &self.parts.headers
332 }
333
334 pub fn headers_mut(&mut self) -> &mut crate::HeaderMap {
336 &mut self.parts.headers
337 }
338
339 #[must_use]
341 pub fn extensions(&self) -> &Extensions {
342 &self.parts.extensions
343 }
344
345 pub fn extensions_mut(&mut self) -> &mut Extensions {
347 &mut self.parts.extensions
348 }
349
350 #[must_use]
352 pub fn body(&self) -> &Body {
353 &self.body
354 }
355
356 pub fn body_mut(&mut self) -> &mut Body {
358 &mut self.body
359 }
360
361 #[must_use]
363 pub fn into_body(self) -> Body {
364 self.body
365 }
366
367 #[must_use]
369 pub fn into_parts(self) -> (crate::response::Parts, Body) {
370 (self.parts, self.body)
371 }
372
373 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 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 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 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 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}