1#[cfg(not(target_arch = "wasm32"))]
2use std::path::Path;
3use std::{
4 future::Future,
5 pin::Pin,
6 sync::{
7 Arc, Mutex, PoisonError, Weak,
8 atomic::{AtomicBool, Ordering},
9 },
10};
11
12use cranpose_core::{CompositionLocal, compositionLocalOfWithPolicy};
13#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
14use futures_util::{StreamExt, stream};
15
16#[derive(thiserror::Error, Debug, Clone)]
17pub enum HttpError {
18 #[error("Failed to build HTTP client: {0}")]
19 ClientInit(String),
20 #[error("Request failed for {url}: {message}")]
21 RequestFailed { url: String, message: String },
22 #[error("Request failed with status {status} for {url}")]
23 HttpStatus { url: String, status: u16 },
24 #[error("Failed to read response body for {url}: {message}")]
25 BodyReadFailed { url: String, message: String },
26 #[error("Invalid response for {url}: {message}")]
27 InvalidResponse { url: String, message: String },
28 #[error("No window object available")]
29 NoWindow,
30 #[error("{operation} worker thread panicked")]
31 WorkerPanicked { operation: &'static str },
32 #[error("{operation} requires cranpose-services feature `{feature}`")]
33 UnsupportedFeature {
34 operation: &'static str,
35 feature: &'static str,
36 },
37 #[error("Download was cancelled")]
38 Cancelled,
39 #[error("Failed to write downloaded file {path}: {message}")]
40 FileWrite {
41 path: String,
42 kind: std::io::ErrorKind,
45 message: String,
46 },
47}
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51pub struct HttpProgress {
52 pub transferred: u64,
54 pub total: Option<u64>,
57}
58
59impl HttpProgress {
60 pub fn fraction(&self) -> Option<f32> {
63 let total = self.total?;
64 if total == 0 {
65 return None;
66 }
67 Some((self.transferred as f32 / total as f32).clamp(0.0, 1.0))
68 }
69
70 pub fn is_complete(&self) -> bool {
72 self.total.is_some_and(|total| self.transferred >= total)
73 }
74}
75
76pub type ProgressHandler = Arc<dyn Fn(HttpProgress) + Send + Sync>;
78
79#[derive(Clone, Default)]
85pub struct HttpControl {
86 cancel: Arc<CancelState>,
87 progress: Option<ProgressHandler>,
88}
89
90#[derive(Default)]
91struct CancelState {
92 cancelled: AtomicBool,
93 waiting: Mutex<Vec<Weak<dyn CancelTransfer>>>,
94}
95
96trait CancelTransfer: Send + Sync {
97 fn cancel(&self);
98}
99
100impl HttpControl {
101 pub fn new() -> Self {
103 Self::default()
104 }
105
106 pub fn with_progress(
112 mut self,
113 progress: impl Fn(HttpProgress) + Send + Sync + 'static,
114 ) -> Self {
115 self.progress = Some(Arc::new(progress));
116 self
117 }
118
119 pub fn cancel(&self) {
127 self.cancel.cancelled.store(true, Ordering::Release);
128 let waiting = std::mem::take(
129 &mut *self
130 .cancel
131 .waiting
132 .lock()
133 .unwrap_or_else(PoisonError::into_inner),
134 );
135 for transfer in waiting.iter().filter_map(Weak::upgrade) {
136 transfer.cancel();
137 }
138 }
139
140 pub fn is_cancelled(&self) -> bool {
142 self.cancel.cancelled.load(Ordering::Acquire)
143 }
144
145 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
146 fn on_cancel(&self, transfer: Weak<dyn CancelTransfer>) {
147 let mut waiting = self
148 .cancel
149 .waiting
150 .lock()
151 .unwrap_or_else(PoisonError::into_inner);
152 if self.is_cancelled() {
153 drop(waiting);
154 if let Some(transfer) = transfer.upgrade() {
155 transfer.cancel();
156 }
157 return;
158 }
159 waiting.retain(|transfer| transfer.strong_count() > 0);
160 waiting.push(transfer);
161 }
162
163 pub fn report(&self, progress: HttpProgress) {
171 if let Some(handler) = &self.progress {
172 handler(progress);
173 }
174 }
175}
176
177#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
179pub enum HttpMethod {
180 #[default]
181 Get,
182 Head,
183 Post,
184 Put,
185 Delete,
186}
187
188impl HttpMethod {
189 pub fn name(self) -> &'static str {
191 match self {
192 HttpMethod::Get => "GET",
193 HttpMethod::Head => "HEAD",
194 HttpMethod::Post => "POST",
195 HttpMethod::Put => "PUT",
196 HttpMethod::Delete => "DELETE",
197 }
198 }
199}
200
201#[derive(Clone, Debug, Default, PartialEq, Eq)]
203pub struct HttpRequest {
204 pub url: String,
205 pub method: HttpMethod,
206 pub headers: Vec<(String, String)>,
207 pub body: Option<Vec<u8>>,
208 pub resume_from: Option<u64>,
215}
216
217impl HttpRequest {
218 pub fn get(url: impl Into<String>) -> Self {
220 Self {
221 url: url.into(),
222 ..Self::default()
223 }
224 }
225
226 pub fn method(mut self, method: HttpMethod) -> Self {
227 self.method = method;
228 self
229 }
230
231 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
232 self.headers.push((name.into(), value.into()));
233 self
234 }
235
236 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
237 self.body = Some(body.into());
238 self
239 }
240
241 pub fn resume_from(mut self, offset: u64) -> Self {
243 self.resume_from = (offset > 0).then_some(offset);
244 self
245 }
246}
247
248#[cfg(not(target_arch = "wasm32"))]
249pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + Send + 'a>>;
250
251#[cfg(target_arch = "wasm32")]
252pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + 'a>>;
253
254pub trait HttpBody {
261 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>>;
263}
264
265#[cfg(not(target_arch = "wasm32"))]
272pub type HttpBodyRef = Arc<dyn HttpBody + Send + Sync>;
273
274#[cfg(target_arch = "wasm32")]
280pub type HttpBodyRef = std::rc::Rc<dyn HttpBody>;
281
282#[cfg(not(target_arch = "wasm32"))]
289pub fn http_body_ref<B: HttpBody + Send + Sync + 'static>(body: B) -> HttpBodyRef {
290 Arc::new(body)
291}
292
293#[cfg(target_arch = "wasm32")]
295pub fn http_body_ref<B: HttpBody + 'static>(body: B) -> HttpBodyRef {
296 std::rc::Rc::new(body)
297}
298
299struct EmptyBody;
300
301impl HttpBody for EmptyBody {
302 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
303 Box::pin(async { Ok(None) })
304 }
305}
306
307pub struct BytesBody {
313 bytes: Vec<u8>,
314 offset: std::sync::Mutex<usize>,
315 chunk: usize,
316}
317
318impl BytesBody {
319 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
321 Self::chunked(bytes, usize::MAX)
322 }
323
324 pub fn chunked(bytes: impl Into<Vec<u8>>, chunk: usize) -> Self {
326 Self {
327 bytes: bytes.into(),
328 offset: std::sync::Mutex::new(0),
329 chunk: chunk.max(1),
330 }
331 }
332
333 pub fn len(&self) -> u64 {
335 self.bytes.len() as u64
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.bytes.is_empty()
341 }
342}
343
344impl HttpBody for BytesBody {
345 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
346 Box::pin(async move {
347 let mut offset = self.offset.lock().unwrap_or_else(PoisonError::into_inner);
348 if *offset >= self.bytes.len() {
349 return Ok(None);
350 }
351 let end = offset.saturating_add(self.chunk).min(self.bytes.len());
352 let chunk = self.bytes[*offset..end].to_vec();
353 *offset = end;
354 Ok(Some(chunk))
355 })
356 }
357}
358
359pub struct HttpResponse {
361 pub status: u16,
363 pub headers: Vec<(String, String)>,
365 pub content_length: Option<u64>,
367 pub resumed: bool,
372 pub url: String,
374 body: HttpBodyRef,
375}
376
377impl HttpResponse {
378 pub fn new(url: impl Into<String>, status: u16, body: HttpBodyRef) -> Self {
380 Self {
381 status,
382 headers: Vec::new(),
383 content_length: None,
384 resumed: false,
385 url: url.into(),
386 body,
387 }
388 }
389
390 pub fn empty(url: impl Into<String>, status: u16) -> Self {
392 Self::new(url, status, http_body_ref(EmptyBody))
393 }
394
395 pub fn with_headers(mut self, headers: Vec<(String, String)>) -> Self {
396 self.headers = headers;
397 self
398 }
399
400 pub fn with_content_length(mut self, content_length: Option<u64>) -> Self {
401 self.content_length = content_length;
402 self
403 }
404
405 pub fn with_resumed(mut self, resumed: bool) -> Self {
406 self.resumed = resumed;
407 self
408 }
409
410 pub fn is_success(&self) -> bool {
412 (200..300).contains(&self.status)
413 }
414
415 pub fn header(&self, name: &str) -> Option<&str> {
417 let name = name.to_ascii_lowercase();
418 self.headers
419 .iter()
420 .find(|(key, _)| key.eq_ignore_ascii_case(&name))
421 .map(|(_, value)| value.as_str())
422 }
423
424 pub fn error_for_status(self) -> Result<Self, HttpError> {
426 if self.is_success() {
427 Ok(self)
428 } else {
429 Err(HttpError::HttpStatus {
430 url: self.url.clone(),
431 status: self.status,
432 })
433 }
434 }
435
436 pub async fn read_chunk(&self) -> Result<Option<Vec<u8>>, HttpError> {
438 self.body.read_chunk().await
439 }
440
441 pub async fn read_all(&self) -> Result<Vec<u8>, HttpError> {
446 let mut out = Vec::with_capacity(self.content_length.unwrap_or(0).min(1 << 20) as usize);
447 while let Some(chunk) = self.read_chunk().await? {
448 out.extend_from_slice(&chunk);
449 }
450 Ok(out)
451 }
452
453 pub async fn read_text(&self) -> Result<String, HttpError> {
455 String::from_utf8(self.read_all().await?).map_err(|error| HttpError::InvalidResponse {
456 url: self.url.clone(),
457 message: error.to_string(),
458 })
459 }
460}
461
462pub trait HttpClient: Send + Sync {
469 fn send<'a>(
472 &'a self,
473 request: &'a HttpRequest,
474 control: HttpControl,
475 ) -> HttpFuture<'a, HttpResponse>;
476
477 fn get_text<'a>(&'a self, url: &'a str) -> HttpFuture<'a, String> {
479 Box::pin(async move {
480 self.send(&HttpRequest::get(url), HttpControl::new())
481 .await?
482 .error_for_status()?
483 .read_text()
484 .await
485 })
486 }
487
488 fn get_bytes<'a>(&'a self, url: &'a str) -> HttpFuture<'a, Vec<u8>> {
490 Box::pin(async move {
491 self.send(&HttpRequest::get(url), HttpControl::new())
492 .await?
493 .error_for_status()?
494 .read_all()
495 .await
496 })
497 }
498
499 #[cfg(not(target_arch = "wasm32"))]
507 fn download_to<'a>(
508 &'a self,
509 url: &'a str,
510 target: &'a Path,
511 control: HttpControl,
512 ) -> HttpFuture<'a, u64> {
513 Box::pin(async move { download_through(self, url, target, control).await })
514 }
515}
516
517#[cfg(not(target_arch = "wasm32"))]
518async fn download_through<C: HttpClient + ?Sized>(
519 client: &C,
520 url: &str,
521 target: &Path,
522 control: HttpControl,
523) -> Result<u64, HttpError> {
524 use std::io::Write;
525
526 if control.is_cancelled() {
527 return Err(HttpError::Cancelled);
528 }
529 let existing = std::fs::metadata(target).map_or(0, |metadata| metadata.len());
530 let mut request = HttpRequest::get(url);
531 if existing > 0 {
532 request = request.resume_from(existing);
533 }
534 let response = client
535 .send(&request, control.clone())
536 .await?
537 .error_for_status()?;
538
539 let base = if response.resumed { existing } else { 0 };
540 let total = response.content_length.map(|remaining| base + remaining);
541 let mut output = if response.resumed {
542 std::fs::OpenOptions::new().append(true).open(target)
543 } else {
544 std::fs::File::create(target)
545 }
546 .map_err(|error| file_write_error(target, &error))?;
547
548 let mut transferred = base;
549 control.report(HttpProgress { transferred, total });
550 while let Some(chunk) = response.read_chunk().await? {
551 if control.is_cancelled() {
552 return Err(HttpError::Cancelled);
553 }
554 output
555 .write_all(&chunk)
556 .map_err(|error| file_write_error(target, &error))?;
557 transferred += chunk.len() as u64;
558 control.report(HttpProgress { transferred, total });
559 }
560 output
561 .flush()
562 .map_err(|error| file_write_error(target, &error))?;
563 Ok(transferred)
564}
565
566#[cfg(not(target_arch = "wasm32"))]
567fn file_write_error(target: &Path, error: &std::io::Error) -> HttpError {
568 HttpError::FileWrite {
569 path: target.display().to_string(),
570 kind: error.kind(),
571 message: error.to_string(),
572 }
573}
574
575pub type HttpClientRef = Arc<dyn HttpClient>;
576
577#[cfg(not(target_arch = "wasm32"))]
578pub async fn map_ordered_concurrent<I, T, F, Fut>(
579 items: &[I],
580 concurrency: usize,
581 task: F,
582) -> Result<Vec<T>, HttpError>
583where
584 I: Clone + Send,
585 T: Send,
586 F: Fn(I) -> Fut + Send + Sync + 'static,
587 Fut: Future<Output = T> + Send,
588{
589 let task = Arc::new(task);
590 let mut results = Vec::with_capacity(items.len());
591
592 for chunk in items.chunks(concurrency.max(1)) {
593 let chunk_results = std::thread::scope(|scope| {
594 let mut handles = Vec::with_capacity(chunk.len());
595 for item in chunk.iter().cloned() {
596 let task = Arc::clone(&task);
597 handles.push(scope.spawn(move || pollster::block_on(task(item))));
598 }
599
600 let mut chunk_results = Vec::with_capacity(handles.len());
601 for handle in handles {
602 let value = handle.join().map_err(|_| HttpError::WorkerPanicked {
603 operation: "ordered concurrent task",
604 })?;
605 chunk_results.push(value);
606 }
607 Ok::<Vec<T>, HttpError>(chunk_results)
608 })?;
609 results.extend(chunk_results);
610 }
611
612 Ok(results)
613}
614
615#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
616pub async fn map_ordered_concurrent<I, T, F, Fut>(
617 items: &[I],
618 concurrency: usize,
619 task: F,
620) -> Result<Vec<T>, HttpError>
621where
622 I: Clone,
623 F: Fn(I) -> Fut + Clone,
624 Fut: Future<Output = T>,
625{
626 let mut results = stream::iter(items.iter().cloned().enumerate().map(|(index, item)| {
627 let task = task.clone();
628 async move { (index, task(item).await) }
629 }))
630 .buffer_unordered(concurrency.max(1))
631 .collect::<Vec<_>>()
632 .await;
633
634 results.sort_by_key(|(index, _)| *index);
635 Ok(results.into_iter().map(|(_, value)| value).collect())
636}
637
638#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
639pub async fn map_ordered_concurrent<I, T, F, Fut>(
640 items: &[I],
641 _concurrency: usize,
642 task: F,
643) -> Result<Vec<T>, HttpError>
644where
645 I: Clone,
646 F: Fn(I) -> Fut,
647 Fut: Future<Output = T>,
648{
649 Ok(map_ordered_sequential(items, task).await)
650}
651
652#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
653async fn map_ordered_sequential<I, T, F, Fut>(items: &[I], task: F) -> Vec<T>
654where
655 I: Clone,
656 F: Fn(I) -> Fut,
657 Fut: Future<Output = T>,
658{
659 let mut results = Vec::with_capacity(items.len());
660 for item in items.iter().cloned() {
661 results.push(task(item).await);
662 }
663 results
664}
665
666pub type StubAnswer = dyn Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync;
668
669pub struct StubHttpClient {
683 answer: Box<StubAnswer>,
684}
685
686impl StubHttpClient {
687 pub fn new(
689 answer: impl Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync + 'static,
690 ) -> Self {
691 Self {
692 answer: Box::new(answer),
693 }
694 }
695
696 pub fn with_body(body: impl Into<Vec<u8>>) -> Self {
698 let body = body.into();
699 Self::new(move |request| {
700 Ok(HttpResponse::new(
701 request.url.clone(),
702 200,
703 http_body_ref(BytesBody::new(body.clone())),
704 ))
705 })
706 }
707
708 pub fn from_text(
710 answer: impl Fn(&str) -> Result<String, HttpError> + Send + Sync + 'static,
711 ) -> Self {
712 Self::new(move |request| {
713 let body = answer(&request.url)?;
714 Ok(HttpResponse::new(
715 request.url.clone(),
716 200,
717 http_body_ref(BytesBody::new(body)),
718 ))
719 })
720 }
721}
722
723impl HttpClient for StubHttpClient {
724 fn send<'a>(
725 &'a self,
726 request: &'a HttpRequest,
727 _control: HttpControl,
728 ) -> HttpFuture<'a, HttpResponse> {
729 Box::pin(async move { (self.answer)(request) })
730 }
731}
732
733#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
734const CHUNK_LEN: usize = 64 * 1024;
735
736#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
737const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
738
739struct DefaultHttpClient {
740 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
741 native_client: Result<reqwest::blocking::Client, HttpError>,
742}
743
744impl DefaultHttpClient {
745 fn new() -> Self {
746 Self {
747 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
748 native_client: build_native_client(IDLE_TIMEOUT),
749 }
750 }
751
752 #[cfg(all(test, not(target_arch = "wasm32"), feature = "http-native"))]
753 fn with_idle_timeout(idle: std::time::Duration) -> Self {
754 Self {
755 native_client: build_native_client(idle),
756 }
757 }
758}
759
760#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
761struct ResponseHead {
762 status: u16,
763 headers: Vec<(String, String)>,
764 content_length: Option<u64>,
765 resumed: bool,
766}
767
768#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
769struct NativeTransfer {
770 head: crate::async_io::Signal<Result<ResponseHead, HttpError>>,
771 chunks: crate::async_io::ChunkStream<HttpError>,
772}
773
774#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
775impl CancelTransfer for NativeTransfer {
776 fn cancel(&self) {
777 self.head.set(Err(HttpError::Cancelled));
778 self.chunks.fail(HttpError::Cancelled);
779 }
780}
781
782#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
783struct ChannelBody {
784 transfer: Arc<NativeTransfer>,
785}
786
787#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
788impl HttpBody for ChannelBody {
789 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
790 Box::pin(self.transfer.chunks.next())
791 }
792}
793
794#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
795async fn send_native(
796 client: reqwest::blocking::Client,
797 request: HttpRequest,
798 control: HttpControl,
799) -> Result<HttpResponse, HttpError> {
800 use crate::async_io::{ChunkChannel, Signal};
801
802 let head = Signal::new();
803 let (chunks, stream) = ChunkChannel::<HttpError>::new();
804 let transfer = Arc::new(NativeTransfer {
805 head: head.clone(),
806 chunks: stream,
807 });
808 control.on_cancel(Arc::<NativeTransfer>::downgrade(&transfer));
809 let url = request.url.clone();
810
811 std::thread::Builder::new()
812 .name("cranpose-http".to_string())
813 .spawn(move || {
814 if let Err(error) = read_native_response(&client, request, &control, &chunks, &head) {
815 head.set(Err(error.clone()));
816 chunks.fail(error);
817 }
818 })
819 .map_err(|error| HttpError::RequestFailed {
820 url: url.clone(),
821 message: format!("could not start the transfer: {error}"),
822 })?;
823
824 let head = transfer
825 .head
826 .wait()
827 .await
828 .ok_or_else(|| HttpError::RequestFailed {
829 url: url.clone(),
830 message: "the transfer ended before a response arrived".to_string(),
831 })??;
832
833 Ok(
834 HttpResponse::new(url, head.status, http_body_ref(ChannelBody { transfer }))
835 .with_headers(head.headers)
836 .with_content_length(head.content_length)
837 .with_resumed(head.resumed),
838 )
839}
840
841#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
842fn read_native_response(
843 client: &reqwest::blocking::Client,
844 request: HttpRequest,
845 control: &HttpControl,
846 chunks: &crate::async_io::ChunkChannel<HttpError>,
847 head_signal: &crate::async_io::Signal<Result<ResponseHead, HttpError>>,
848) -> Result<(), HttpError> {
849 use std::io::Read;
850
851 if control.is_cancelled() {
852 return Err(HttpError::Cancelled);
853 }
854 let HttpRequest {
855 url,
856 method,
857 headers,
858 body,
859 resume_from,
860 } = request;
861 let method = match method {
862 HttpMethod::Get => reqwest::Method::GET,
863 HttpMethod::Head => reqwest::Method::HEAD,
864 HttpMethod::Post => reqwest::Method::POST,
865 HttpMethod::Put => reqwest::Method::PUT,
866 HttpMethod::Delete => reqwest::Method::DELETE,
867 };
868 let mut builder = client.request(method, &url);
869 for (name, value) in headers {
870 builder = builder.header(name, value);
871 }
872 if let Some(offset) = resume_from {
873 builder = builder.header(reqwest::header::RANGE, format!("bytes={offset}-"));
874 }
875 if let Some(body) = body {
876 builder = builder.body(body);
877 }
878
879 let mut response = builder.send().map_err(|error| HttpError::RequestFailed {
880 url: url.clone(),
881 message: error.to_string(),
882 })?;
883
884 let status = response.status();
885 let resumed = resume_from.is_some() && status == reqwest::StatusCode::PARTIAL_CONTENT;
886 let content_length = response.content_length();
887 let headers = response
888 .headers()
889 .iter()
890 .map(|(name, value)| {
891 (
892 name.as_str().to_ascii_lowercase(),
893 value.to_str().unwrap_or_default().to_string(),
894 )
895 })
896 .collect();
897 head_signal.set(Ok(ResponseHead {
898 status: status.as_u16(),
899 headers,
900 content_length,
901 resumed,
902 }));
903
904 let mut buffer = vec![0u8; CHUNK_LEN];
905 let mut transferred = 0u64;
906 loop {
907 if control.is_cancelled() {
908 return Err(HttpError::Cancelled);
909 }
910 let count = response
911 .read(&mut buffer)
912 .map_err(|error| HttpError::BodyReadFailed {
913 url: url.clone(),
914 message: error.to_string(),
915 })?;
916 if count == 0 {
917 break;
918 }
919 transferred += count as u64;
920 control.report(HttpProgress {
921 transferred,
922 total: content_length,
923 });
924 if !chunks.push(buffer[..count].to_vec()) {
925 break;
926 }
927 }
928 Ok(())
929}
930
931#[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
932async fn send_native(
933 _request: HttpRequest,
934 _control: HttpControl,
935) -> Result<HttpResponse, HttpError> {
936 Err(HttpError::UnsupportedFeature {
937 operation: "native HTTP requests",
938 feature: "http-native",
939 })
940}
941
942impl HttpClient for DefaultHttpClient {
943 fn send<'a>(
944 &'a self,
945 request: &'a HttpRequest,
946 control: HttpControl,
947 ) -> HttpFuture<'a, HttpResponse> {
948 Box::pin(async move {
949 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
950 {
951 let client = self.native_client.as_ref().map_err(Clone::clone)?.clone();
952 send_native(client, request.clone(), control).await
953 }
954 #[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
955 {
956 send_native(request.clone(), control).await
957 }
958 #[cfg(target_arch = "wasm32")]
959 {
960 send_web(request, control).await
961 }
962 })
963 }
964}
965
966#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
967fn build_native_client(idle: std::time::Duration) -> Result<reqwest::blocking::Client, HttpError> {
968 configure_native_client_builder(
969 reqwest::blocking::Client::builder()
970 .connect_timeout(std::time::Duration::from_secs(30))
971 .timeout(idle)
972 .user_agent(concat!("cranpose/", env!("CARGO_PKG_VERSION"))),
973 )?
974 .build()
975 .map_err(|err| HttpError::ClientInit(err.to_string()))
976}
977
978#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
979fn configure_native_client_builder(
980 builder: reqwest::blocking::ClientBuilder,
981) -> Result<reqwest::blocking::ClientBuilder, HttpError> {
982 #[cfg(target_os = "android")]
983 {
984 Ok(builder.tls_certs_only(android_root_certificates()?))
985 }
986
987 #[cfg(not(target_os = "android"))]
988 {
989 Ok(builder)
990 }
991}
992
993#[cfg(all(target_os = "android", feature = "http-native"))]
994fn android_root_certificates() -> Result<Vec<reqwest::Certificate>, HttpError> {
995 certificates_from_der_chain(
996 webpki_root_certs::TLS_SERVER_ROOT_CERTS
997 .iter()
998 .map(AsRef::as_ref),
999 )
1000}
1001
1002#[cfg(any(
1003 all(test, not(target_arch = "wasm32"), feature = "http-native"),
1004 all(target_os = "android", feature = "http-native")
1005))]
1006fn certificates_from_der_chain<'a, I>(
1007 certificates: I,
1008) -> Result<Vec<reqwest::Certificate>, HttpError>
1009where
1010 I: IntoIterator<Item = &'a [u8]>,
1011{
1012 certificates
1013 .into_iter()
1014 .enumerate()
1015 .map(|(index, der)| {
1016 reqwest::Certificate::from_der(der).map_err(|err| {
1017 HttpError::ClientInit(format!(
1018 "Failed to load TLS root certificate {index}: {err}"
1019 ))
1020 })
1021 })
1022 .collect()
1023}
1024
1025#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1026struct FetchBody {
1027 url: String,
1028 reader: web_sys::ReadableStreamDefaultReader,
1029 control: HttpControl,
1030 transferred: std::cell::Cell<u64>,
1031 total: Option<u64>,
1032}
1033
1034#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1035impl HttpBody for FetchBody {
1036 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
1037 use wasm_bindgen_futures::JsFuture;
1038
1039 Box::pin(async move {
1040 if self.control.is_cancelled() {
1041 let _ = self.reader.cancel();
1042 return Err(HttpError::Cancelled);
1043 }
1044 let result = JsFuture::from(self.reader.read()).await.map_err(|error| {
1045 HttpError::BodyReadFailed {
1046 url: self.url.clone(),
1047 message: format!("{error:?}"),
1048 }
1049 })?;
1050 let done = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("done"))
1051 .ok()
1052 .and_then(|value| value.as_bool())
1053 .unwrap_or(true);
1054 if done {
1055 return Ok(None);
1056 }
1057 let value = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("value"))
1058 .map_err(|error| HttpError::BodyReadFailed {
1059 url: self.url.clone(),
1060 message: format!("{error:?}"),
1061 })?;
1062 let chunk = js_sys::Uint8Array::new(&value).to_vec();
1063 self.transferred
1064 .set(self.transferred.get() + chunk.len() as u64);
1065 self.control.report(HttpProgress {
1066 transferred: self.transferred.get(),
1067 total: self.total,
1068 });
1069 Ok(Some(chunk))
1070 })
1071 }
1072}
1073
1074#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1075async fn send_web(request: &HttpRequest, control: HttpControl) -> Result<HttpResponse, HttpError> {
1076 use wasm_bindgen::JsCast;
1077 use wasm_bindgen_futures::JsFuture;
1078 use web_sys::{Request, RequestInit, RequestMode, Response};
1079
1080 if control.is_cancelled() {
1081 return Err(HttpError::Cancelled);
1082 }
1083 let options = RequestInit::new();
1084 options.set_method(request.method.name());
1085 options.set_mode(RequestMode::Cors);
1086 if let Some(body) = &request.body {
1087 options.set_body(&js_sys::Uint8Array::from(body.as_slice()).into());
1088 }
1089
1090 let fetch_request =
1091 Request::new_with_str_and_init(&request.url, &options).map_err(|error| {
1092 HttpError::RequestFailed {
1093 url: request.url.clone(),
1094 message: format!("{error:?}"),
1095 }
1096 })?;
1097 let headers = fetch_request.headers();
1098 for (name, value) in &request.headers {
1099 headers
1100 .set(name, value)
1101 .map_err(|error| HttpError::RequestFailed {
1102 url: request.url.clone(),
1103 message: format!("{error:?}"),
1104 })?;
1105 }
1106 if let Some(offset) = request.resume_from {
1107 headers
1108 .set("Range", &format!("bytes={offset}-"))
1109 .map_err(|error| HttpError::RequestFailed {
1110 url: request.url.clone(),
1111 message: format!("{error:?}"),
1112 })?;
1113 }
1114
1115 let window = web_sys::window().ok_or(HttpError::NoWindow)?;
1116 let value = JsFuture::from(window.fetch_with_request(&fetch_request))
1117 .await
1118 .map_err(|error| HttpError::RequestFailed {
1119 url: request.url.clone(),
1120 message: format!("{error:?}"),
1121 })?;
1122 let response: Response = value.dyn_into().map_err(|_| HttpError::InvalidResponse {
1123 url: request.url.clone(),
1124 message: "the browser answered with something that is not a Response".to_string(),
1125 })?;
1126
1127 let status = response.status();
1128 let mut header_pairs = Vec::new();
1129 let entries = js_sys::try_iter(&response.headers()).ok().flatten();
1130 if let Some(entries) = entries {
1131 for entry in entries.flatten() {
1132 let pair = js_sys::Array::from(&entry);
1133 if pair.length() >= 2 {
1134 let name = pair.get(0).as_string().unwrap_or_default();
1135 let value = pair.get(1).as_string().unwrap_or_default();
1136 header_pairs.push((name.to_ascii_lowercase(), value));
1137 }
1138 }
1139 }
1140 let total = header_pairs
1141 .iter()
1142 .find(|(name, _)| name == "content-length")
1143 .and_then(|(_, value)| value.parse::<u64>().ok());
1144 let resumed = request.resume_from.is_some() && status == 206;
1145
1146 let body = response.body().ok_or_else(|| HttpError::InvalidResponse {
1147 url: request.url.clone(),
1148 message: "the response carries no body".to_string(),
1149 })?;
1150 let reader: web_sys::ReadableStreamDefaultReader =
1151 body.get_reader()
1152 .dyn_into()
1153 .map_err(|_| HttpError::InvalidResponse {
1154 url: request.url.clone(),
1155 message: "the response body cannot be read in chunks".to_string(),
1156 })?;
1157
1158 Ok(HttpResponse::new(
1159 request.url.clone(),
1160 status,
1161 http_body_ref(FetchBody {
1162 url: request.url.clone(),
1163 reader,
1164 control,
1165 transferred: std::cell::Cell::new(0),
1166 total,
1167 }),
1168 )
1169 .with_headers(header_pairs)
1170 .with_content_length(total)
1171 .with_resumed(resumed))
1172}
1173
1174#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
1175async fn send_web(request: &HttpRequest, _control: HttpControl) -> Result<HttpResponse, HttpError> {
1176 let _ = request;
1177 Err(HttpError::UnsupportedFeature {
1178 operation: "web HTTP requests",
1179 feature: "web-http",
1180 })
1181}
1182
1183pub fn default_http_client() -> HttpClientRef {
1191 Arc::new(DefaultHttpClient::new())
1192}
1193
1194pub fn local_http_client() -> CompositionLocal<HttpClientRef> {
1195 crate::composition_locals::cached_local(
1196 |locals| &locals.http,
1197 || compositionLocalOfWithPolicy(default_http_client, Arc::ptr_eq),
1198 )
1199}
1200
1201#[cfg(test)]
1202#[path = "tests/http_tests.rs"]
1203mod tests;