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
96pub trait CancelTransfer: Send + Sync {
99 fn cancel(&self);
101}
102
103impl HttpControl {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn with_progress(
115 mut self,
116 progress: impl Fn(HttpProgress) + Send + Sync + 'static,
117 ) -> Self {
118 self.progress = Some(Arc::new(progress));
119 self
120 }
121
122 pub fn cancel(&self) {
130 self.cancel.cancelled.store(true, Ordering::Release);
131 let waiting = std::mem::take(
132 &mut *self
133 .cancel
134 .waiting
135 .lock()
136 .unwrap_or_else(PoisonError::into_inner),
137 );
138 for transfer in waiting.iter().filter_map(Weak::upgrade) {
139 transfer.cancel();
140 }
141 }
142
143 #[cfg(not(target_arch = "wasm32"))]
145 fn cancel_only(&self) -> Self {
146 Self {
147 cancel: Arc::clone(&self.cancel),
148 progress: None,
149 }
150 }
151
152 pub fn is_cancelled(&self) -> bool {
154 self.cancel.cancelled.load(Ordering::Acquire)
155 }
156
157 pub fn on_cancel(&self, transfer: Weak<dyn CancelTransfer>) {
161 let mut waiting = self
162 .cancel
163 .waiting
164 .lock()
165 .unwrap_or_else(PoisonError::into_inner);
166 if self.is_cancelled() {
167 drop(waiting);
168 if let Some(transfer) = transfer.upgrade() {
169 transfer.cancel();
170 }
171 return;
172 }
173 waiting.retain(|transfer| transfer.strong_count() > 0);
174 waiting.push(transfer);
175 }
176
177 pub fn report(&self, progress: HttpProgress) {
185 if let Some(handler) = &self.progress {
186 handler(progress);
187 }
188 }
189}
190
191#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
193pub enum HttpMethod {
194 #[default]
195 Get,
196 Head,
197 Post,
198 Put,
199 Delete,
200}
201
202impl HttpMethod {
203 pub fn name(self) -> &'static str {
205 match self {
206 HttpMethod::Get => "GET",
207 HttpMethod::Head => "HEAD",
208 HttpMethod::Post => "POST",
209 HttpMethod::Put => "PUT",
210 HttpMethod::Delete => "DELETE",
211 }
212 }
213}
214
215#[derive(Clone, Debug, Default, PartialEq, Eq)]
217pub struct HttpRequest {
218 pub url: String,
219 pub method: HttpMethod,
220 pub headers: Vec<(String, String)>,
221 pub body: Option<Vec<u8>>,
222 pub resume_from: Option<u64>,
229}
230
231impl HttpRequest {
232 pub fn get(url: impl Into<String>) -> Self {
234 Self {
235 url: url.into(),
236 ..Self::default()
237 }
238 }
239
240 pub fn method(mut self, method: HttpMethod) -> Self {
241 self.method = method;
242 self
243 }
244
245 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
246 self.headers.push((name.into(), value.into()));
247 self
248 }
249
250 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
251 self.body = Some(body.into());
252 self
253 }
254
255 pub fn resume_from(mut self, offset: u64) -> Self {
257 self.resume_from = (offset > 0).then_some(offset);
258 self
259 }
260}
261
262#[cfg(not(target_arch = "wasm32"))]
263pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + Send + 'a>>;
264
265#[cfg(target_arch = "wasm32")]
266pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + 'a>>;
267
268pub trait HttpBody {
275 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>>;
277}
278
279#[cfg(not(target_arch = "wasm32"))]
286pub type HttpBodyRef = Arc<dyn HttpBody + Send + Sync>;
287
288#[cfg(target_arch = "wasm32")]
294pub type HttpBodyRef = std::rc::Rc<dyn HttpBody>;
295
296#[cfg(not(target_arch = "wasm32"))]
303pub fn http_body_ref<B: HttpBody + Send + Sync + 'static>(body: B) -> HttpBodyRef {
304 Arc::new(body)
305}
306
307#[cfg(target_arch = "wasm32")]
309pub fn http_body_ref<B: HttpBody + 'static>(body: B) -> HttpBodyRef {
310 std::rc::Rc::new(body)
311}
312
313struct EmptyBody;
314
315impl HttpBody for EmptyBody {
316 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
317 Box::pin(async { Ok(None) })
318 }
319}
320
321pub struct BytesBody {
327 bytes: Vec<u8>,
328 offset: std::sync::Mutex<usize>,
329 chunk: usize,
330}
331
332impl BytesBody {
333 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
335 Self::chunked(bytes, usize::MAX)
336 }
337
338 pub fn chunked(bytes: impl Into<Vec<u8>>, chunk: usize) -> Self {
340 Self {
341 bytes: bytes.into(),
342 offset: std::sync::Mutex::new(0),
343 chunk: chunk.max(1),
344 }
345 }
346
347 pub fn len(&self) -> u64 {
349 self.bytes.len() as u64
350 }
351
352 pub fn is_empty(&self) -> bool {
354 self.bytes.is_empty()
355 }
356}
357
358impl HttpBody for BytesBody {
359 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
360 Box::pin(async move {
361 let mut offset = self.offset.lock().unwrap_or_else(PoisonError::into_inner);
362 if *offset >= self.bytes.len() {
363 return Ok(None);
364 }
365 let end = offset.saturating_add(self.chunk).min(self.bytes.len());
366 let chunk = self.bytes[*offset..end].to_vec();
367 *offset = end;
368 Ok(Some(chunk))
369 })
370 }
371}
372
373pub struct HttpResponse {
375 pub status: u16,
377 pub headers: Vec<(String, String)>,
379 pub content_length: Option<u64>,
381 pub resumed: bool,
386 pub url: String,
388 body: HttpBodyRef,
389}
390
391impl HttpResponse {
392 pub fn new(url: impl Into<String>, status: u16, body: HttpBodyRef) -> Self {
394 Self {
395 status,
396 headers: Vec::new(),
397 content_length: None,
398 resumed: false,
399 url: url.into(),
400 body,
401 }
402 }
403
404 pub fn empty(url: impl Into<String>, status: u16) -> Self {
406 Self::new(url, status, http_body_ref(EmptyBody))
407 }
408
409 pub fn with_headers(mut self, headers: Vec<(String, String)>) -> Self {
410 self.headers = headers;
411 self
412 }
413
414 pub fn with_content_length(mut self, content_length: Option<u64>) -> Self {
415 self.content_length = content_length;
416 self
417 }
418
419 pub fn with_resumed(mut self, resumed: bool) -> Self {
420 self.resumed = resumed;
421 self
422 }
423
424 pub fn is_success(&self) -> bool {
426 (200..300).contains(&self.status)
427 }
428
429 pub fn header(&self, name: &str) -> Option<&str> {
431 let name = name.to_ascii_lowercase();
432 self.headers
433 .iter()
434 .find(|(key, _)| key.eq_ignore_ascii_case(&name))
435 .map(|(_, value)| value.as_str())
436 }
437
438 pub fn error_for_status(self) -> Result<Self, HttpError> {
440 if self.is_success() {
441 Ok(self)
442 } else {
443 Err(HttpError::HttpStatus {
444 url: self.url.clone(),
445 status: self.status,
446 })
447 }
448 }
449
450 pub async fn read_chunk(&self) -> Result<Option<Vec<u8>>, HttpError> {
452 self.body.read_chunk().await
453 }
454
455 pub async fn read_all(&self) -> Result<Vec<u8>, HttpError> {
460 let mut out = Vec::with_capacity(self.content_length.unwrap_or(0).min(1 << 20) as usize);
461 while let Some(chunk) = self.read_chunk().await? {
462 out.extend_from_slice(&chunk);
463 }
464 Ok(out)
465 }
466
467 pub async fn read_text(&self) -> Result<String, HttpError> {
469 String::from_utf8(self.read_all().await?).map_err(|error| HttpError::InvalidResponse {
470 url: self.url.clone(),
471 message: error.to_string(),
472 })
473 }
474}
475
476pub trait HttpClient: Send + Sync {
483 fn send<'a>(
486 &'a self,
487 request: &'a HttpRequest,
488 control: HttpControl,
489 ) -> HttpFuture<'a, HttpResponse>;
490
491 fn get_text<'a>(&'a self, url: &'a str) -> HttpFuture<'a, String> {
493 Box::pin(async move {
494 self.send(&HttpRequest::get(url), HttpControl::new())
495 .await?
496 .error_for_status()?
497 .read_text()
498 .await
499 })
500 }
501
502 fn get_bytes<'a>(&'a self, url: &'a str) -> HttpFuture<'a, Vec<u8>> {
504 Box::pin(async move {
505 self.send(&HttpRequest::get(url), HttpControl::new())
506 .await?
507 .error_for_status()?
508 .read_all()
509 .await
510 })
511 }
512
513 #[cfg(not(target_arch = "wasm32"))]
521 fn download_to<'a>(
522 &'a self,
523 url: &'a str,
524 target: &'a Path,
525 control: HttpControl,
526 ) -> HttpFuture<'a, u64> {
527 Box::pin(async move { download_through(self, url, target, control).await })
528 }
529}
530
531#[cfg(not(target_arch = "wasm32"))]
540pub trait PlatformDownloader: Send + Sync {
541 fn download<'a>(
542 &'a self,
543 url: &'a str,
544 target: &'a Path,
545 control: HttpControl,
546 ) -> HttpFuture<'a, u64>;
547
548 fn downloaded_length(&self, target: &Path) -> Option<u64>;
552}
553
554#[cfg(not(target_arch = "wasm32"))]
559pub fn downloaded_length(target: &Path) -> u64 {
560 PLATFORM_DOWNLOADER
561 .get()
562 .and_then(|downloader| downloader.downloaded_length(target))
563 .unwrap_or_else(|| std::fs::metadata(target).map_or(0, |metadata| metadata.len()))
564}
565
566#[cfg(not(target_arch = "wasm32"))]
567static PLATFORM_DOWNLOADER: crate::registry::ServiceRegistry<dyn PlatformDownloader> =
568 crate::registry::ServiceRegistry::new();
569
570#[cfg(not(target_arch = "wasm32"))]
575pub fn set_platform_downloader(downloader: Arc<dyn PlatformDownloader>) {
576 PLATFORM_DOWNLOADER.set(downloader);
577}
578
579#[cfg(not(target_arch = "wasm32"))]
580async fn download_through<C: HttpClient + ?Sized>(
581 client: &C,
582 url: &str,
583 target: &Path,
584 control: HttpControl,
585) -> Result<u64, HttpError> {
586 use std::io::Write;
587
588 if control.is_cancelled() {
589 return Err(HttpError::Cancelled);
590 }
591 let existing = std::fs::metadata(target).map_or(0, |metadata| metadata.len());
592 let mut request = HttpRequest::get(url);
593 if existing > 0 {
594 request = request.resume_from(existing);
595 }
596 let response = client
599 .send(&request, control.cancel_only())
600 .await?
601 .error_for_status()?;
602
603 let base = if response.resumed { existing } else { 0 };
604 let total = response.content_length.map(|remaining| base + remaining);
605 let mut output = if response.resumed {
606 std::fs::OpenOptions::new().append(true).open(target)
607 } else {
608 std::fs::File::create(target)
609 }
610 .map_err(|error| file_write_error(target, &error))?;
611
612 let mut transferred = base;
613 control.report(HttpProgress { transferred, total });
614 while let Some(chunk) = response.read_chunk().await? {
615 if control.is_cancelled() {
616 return Err(HttpError::Cancelled);
617 }
618 output
619 .write_all(&chunk)
620 .map_err(|error| file_write_error(target, &error))?;
621 transferred += chunk.len() as u64;
622 control.report(HttpProgress { transferred, total });
623 }
624 output
625 .flush()
626 .map_err(|error| file_write_error(target, &error))?;
627 Ok(transferred)
628}
629
630#[cfg(not(target_arch = "wasm32"))]
631fn file_write_error(target: &Path, error: &std::io::Error) -> HttpError {
632 HttpError::FileWrite {
633 path: target.display().to_string(),
634 kind: error.kind(),
635 message: error.to_string(),
636 }
637}
638
639pub type HttpClientRef = Arc<dyn HttpClient>;
640
641#[cfg(not(target_arch = "wasm32"))]
642pub async fn map_ordered_concurrent<I, T, F, Fut>(
643 items: &[I],
644 concurrency: usize,
645 task: F,
646) -> Result<Vec<T>, HttpError>
647where
648 I: Clone + Send,
649 T: Send,
650 F: Fn(I) -> Fut + Send + Sync + 'static,
651 Fut: Future<Output = T> + Send,
652{
653 let task = Arc::new(task);
654 let mut results = Vec::with_capacity(items.len());
655
656 for chunk in items.chunks(concurrency.max(1)) {
657 let chunk_results = std::thread::scope(|scope| {
658 let mut handles = Vec::with_capacity(chunk.len());
659 for item in chunk.iter().cloned() {
660 let task = Arc::clone(&task);
661 handles.push(scope.spawn(move || pollster::block_on(task(item))));
662 }
663
664 let mut chunk_results = Vec::with_capacity(handles.len());
665 for handle in handles {
666 let value = handle.join().map_err(|_| HttpError::WorkerPanicked {
667 operation: "ordered concurrent task",
668 })?;
669 chunk_results.push(value);
670 }
671 Ok::<Vec<T>, HttpError>(chunk_results)
672 })?;
673 results.extend(chunk_results);
674 }
675
676 Ok(results)
677}
678
679#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
680pub async fn map_ordered_concurrent<I, T, F, Fut>(
681 items: &[I],
682 concurrency: usize,
683 task: F,
684) -> Result<Vec<T>, HttpError>
685where
686 I: Clone,
687 F: Fn(I) -> Fut + Clone,
688 Fut: Future<Output = T>,
689{
690 let mut results = stream::iter(items.iter().cloned().enumerate().map(|(index, item)| {
691 let task = task.clone();
692 async move { (index, task(item).await) }
693 }))
694 .buffer_unordered(concurrency.max(1))
695 .collect::<Vec<_>>()
696 .await;
697
698 results.sort_by_key(|(index, _)| *index);
699 Ok(results.into_iter().map(|(_, value)| value).collect())
700}
701
702#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
703pub async fn map_ordered_concurrent<I, T, F, Fut>(
704 items: &[I],
705 _concurrency: usize,
706 task: F,
707) -> Result<Vec<T>, HttpError>
708where
709 I: Clone,
710 F: Fn(I) -> Fut,
711 Fut: Future<Output = T>,
712{
713 Ok(map_ordered_sequential(items, task).await)
714}
715
716#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
717async fn map_ordered_sequential<I, T, F, Fut>(items: &[I], task: F) -> Vec<T>
718where
719 I: Clone,
720 F: Fn(I) -> Fut,
721 Fut: Future<Output = T>,
722{
723 let mut results = Vec::with_capacity(items.len());
724 for item in items.iter().cloned() {
725 results.push(task(item).await);
726 }
727 results
728}
729
730pub type StubAnswer = dyn Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync;
732
733pub struct StubHttpClient {
747 answer: Box<StubAnswer>,
748}
749
750impl StubHttpClient {
751 pub fn new(
753 answer: impl Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync + 'static,
754 ) -> Self {
755 Self {
756 answer: Box::new(answer),
757 }
758 }
759
760 pub fn with_body(body: impl Into<Vec<u8>>) -> Self {
762 let body = body.into();
763 Self::new(move |request| {
764 Ok(HttpResponse::new(
765 request.url.clone(),
766 200,
767 http_body_ref(BytesBody::new(body.clone())),
768 ))
769 })
770 }
771
772 pub fn from_text(
774 answer: impl Fn(&str) -> Result<String, HttpError> + Send + Sync + 'static,
775 ) -> Self {
776 Self::new(move |request| {
777 let body = answer(&request.url)?;
778 Ok(HttpResponse::new(
779 request.url.clone(),
780 200,
781 http_body_ref(BytesBody::new(body)),
782 ))
783 })
784 }
785}
786
787impl HttpClient for StubHttpClient {
788 fn send<'a>(
789 &'a self,
790 request: &'a HttpRequest,
791 _control: HttpControl,
792 ) -> HttpFuture<'a, HttpResponse> {
793 Box::pin(async move { (self.answer)(request) })
794 }
795}
796
797#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
798const CHUNK_LEN: usize = 64 * 1024;
799
800#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
801const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
802
803struct DefaultHttpClient {
804 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
805 native_client: Result<reqwest::blocking::Client, HttpError>,
806}
807
808impl DefaultHttpClient {
809 fn new() -> Self {
810 Self {
811 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
812 native_client: build_native_client(IDLE_TIMEOUT),
813 }
814 }
815
816 #[cfg(all(test, not(target_arch = "wasm32"), feature = "http-native"))]
817 fn with_idle_timeout(idle: std::time::Duration) -> Self {
818 Self {
819 native_client: build_native_client(idle),
820 }
821 }
822}
823
824#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
825struct ResponseHead {
826 status: u16,
827 headers: Vec<(String, String)>,
828 content_length: Option<u64>,
829 resumed: bool,
830}
831
832#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
833struct NativeTransfer {
834 head: crate::async_io::Signal<Result<ResponseHead, HttpError>>,
835 chunks: crate::async_io::ChunkStream<HttpError>,
836}
837
838#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
839impl CancelTransfer for NativeTransfer {
840 fn cancel(&self) {
841 self.head.set(Err(HttpError::Cancelled));
842 self.chunks.fail(HttpError::Cancelled);
843 }
844}
845
846#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
847struct ChannelBody {
848 transfer: Arc<NativeTransfer>,
849}
850
851#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
852impl HttpBody for ChannelBody {
853 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
854 Box::pin(self.transfer.chunks.next())
855 }
856}
857
858#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
859async fn send_native(
860 client: reqwest::blocking::Client,
861 request: HttpRequest,
862 control: HttpControl,
863) -> Result<HttpResponse, HttpError> {
864 use crate::async_io::{ChunkChannel, Signal};
865
866 let head = Signal::new();
867 let (chunks, stream) = ChunkChannel::<HttpError>::new();
868 let transfer = Arc::new(NativeTransfer {
869 head: head.clone(),
870 chunks: stream,
871 });
872 control.on_cancel(Arc::<NativeTransfer>::downgrade(&transfer));
873 let url = request.url.clone();
874
875 std::thread::Builder::new()
876 .name("cranpose-http".to_string())
877 .spawn(move || {
878 if let Err(error) = read_native_response(&client, request, &control, &chunks, &head) {
879 head.set(Err(error.clone()));
880 chunks.fail(error);
881 }
882 })
883 .map_err(|error| HttpError::RequestFailed {
884 url: url.clone(),
885 message: format!("could not start the transfer: {error}"),
886 })?;
887
888 let head = transfer
889 .head
890 .wait()
891 .await
892 .ok_or_else(|| HttpError::RequestFailed {
893 url: url.clone(),
894 message: "the transfer ended before a response arrived".to_string(),
895 })??;
896
897 Ok(
898 HttpResponse::new(url, head.status, http_body_ref(ChannelBody { transfer }))
899 .with_headers(head.headers)
900 .with_content_length(head.content_length)
901 .with_resumed(head.resumed),
902 )
903}
904
905#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
906fn read_native_response(
907 client: &reqwest::blocking::Client,
908 request: HttpRequest,
909 control: &HttpControl,
910 chunks: &crate::async_io::ChunkChannel<HttpError>,
911 head_signal: &crate::async_io::Signal<Result<ResponseHead, HttpError>>,
912) -> Result<(), HttpError> {
913 use std::io::Read;
914
915 if control.is_cancelled() {
916 return Err(HttpError::Cancelled);
917 }
918 let HttpRequest {
919 url,
920 method,
921 headers,
922 body,
923 resume_from,
924 } = request;
925 let method = match method {
926 HttpMethod::Get => reqwest::Method::GET,
927 HttpMethod::Head => reqwest::Method::HEAD,
928 HttpMethod::Post => reqwest::Method::POST,
929 HttpMethod::Put => reqwest::Method::PUT,
930 HttpMethod::Delete => reqwest::Method::DELETE,
931 };
932 let mut builder = client.request(method, &url);
933 for (name, value) in headers {
934 builder = builder.header(name, value);
935 }
936 if let Some(offset) = resume_from {
937 builder = builder.header(reqwest::header::RANGE, format!("bytes={offset}-"));
938 }
939 if let Some(body) = body {
940 builder = builder.body(body);
941 }
942
943 let mut response = builder.send().map_err(|error| HttpError::RequestFailed {
944 url: url.clone(),
945 message: error.to_string(),
946 })?;
947
948 let status = response.status();
949 let resumed = resume_from.is_some() && status == reqwest::StatusCode::PARTIAL_CONTENT;
950 let content_length = response.content_length();
951 let headers = response
952 .headers()
953 .iter()
954 .map(|(name, value)| {
955 (
956 name.as_str().to_ascii_lowercase(),
957 value.to_str().unwrap_or_default().to_string(),
958 )
959 })
960 .collect();
961 head_signal.set(Ok(ResponseHead {
962 status: status.as_u16(),
963 headers,
964 content_length,
965 resumed,
966 }));
967
968 let mut buffer = vec![0u8; CHUNK_LEN];
969 let mut transferred = 0u64;
970 loop {
971 if control.is_cancelled() {
972 return Err(HttpError::Cancelled);
973 }
974 let count = response
975 .read(&mut buffer)
976 .map_err(|error| HttpError::BodyReadFailed {
977 url: url.clone(),
978 message: error.to_string(),
979 })?;
980 if count == 0 {
981 break;
982 }
983 transferred += count as u64;
984 control.report(HttpProgress {
985 transferred,
986 total: content_length,
987 });
988 if !chunks.push(buffer[..count].to_vec()) {
989 break;
990 }
991 }
992 Ok(())
993}
994
995#[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
996async fn send_native(
997 _request: HttpRequest,
998 _control: HttpControl,
999) -> Result<HttpResponse, HttpError> {
1000 Err(HttpError::UnsupportedFeature {
1001 operation: "native HTTP requests",
1002 feature: "http-native",
1003 })
1004}
1005
1006impl HttpClient for DefaultHttpClient {
1007 fn send<'a>(
1008 &'a self,
1009 request: &'a HttpRequest,
1010 control: HttpControl,
1011 ) -> HttpFuture<'a, HttpResponse> {
1012 Box::pin(async move {
1013 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1014 {
1015 let client = self.native_client.as_ref().map_err(Clone::clone)?.clone();
1016 send_native(client, request.clone(), control).await
1017 }
1018 #[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
1019 {
1020 send_native(request.clone(), control).await
1021 }
1022 #[cfg(target_arch = "wasm32")]
1023 {
1024 send_web(request, control).await
1025 }
1026 })
1027 }
1028
1029 #[cfg(not(target_arch = "wasm32"))]
1030 fn download_to<'a>(
1031 &'a self,
1032 url: &'a str,
1033 target: &'a Path,
1034 control: HttpControl,
1035 ) -> HttpFuture<'a, u64> {
1036 Box::pin(async move {
1037 match PLATFORM_DOWNLOADER.get() {
1038 Some(downloader) => downloader.download(url, target, control).await,
1039 None => download_through(self, url, target, control).await,
1040 }
1041 })
1042 }
1043}
1044
1045#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1046fn build_native_client(idle: std::time::Duration) -> Result<reqwest::blocking::Client, HttpError> {
1047 configure_native_client_builder(
1048 reqwest::blocking::Client::builder()
1049 .connect_timeout(std::time::Duration::from_secs(30))
1050 .timeout(idle)
1051 .user_agent(concat!("cranpose/", env!("CARGO_PKG_VERSION"))),
1052 )?
1053 .build()
1054 .map_err(|err| HttpError::ClientInit(err.to_string()))
1055}
1056
1057#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1058fn configure_native_client_builder(
1059 builder: reqwest::blocking::ClientBuilder,
1060) -> Result<reqwest::blocking::ClientBuilder, HttpError> {
1061 #[cfg(target_os = "android")]
1062 {
1063 Ok(builder.tls_certs_only(android_root_certificates()?))
1064 }
1065
1066 #[cfg(not(target_os = "android"))]
1067 {
1068 Ok(builder)
1069 }
1070}
1071
1072#[cfg(all(target_os = "android", feature = "http-native"))]
1073fn android_root_certificates() -> Result<Vec<reqwest::Certificate>, HttpError> {
1074 certificates_from_der_chain(
1075 webpki_root_certs::TLS_SERVER_ROOT_CERTS
1076 .iter()
1077 .map(AsRef::as_ref),
1078 )
1079}
1080
1081#[cfg(any(
1082 all(test, not(target_arch = "wasm32"), feature = "http-native"),
1083 all(target_os = "android", feature = "http-native")
1084))]
1085fn certificates_from_der_chain<'a, I>(
1086 certificates: I,
1087) -> Result<Vec<reqwest::Certificate>, HttpError>
1088where
1089 I: IntoIterator<Item = &'a [u8]>,
1090{
1091 certificates
1092 .into_iter()
1093 .enumerate()
1094 .map(|(index, der)| {
1095 reqwest::Certificate::from_der(der).map_err(|err| {
1096 HttpError::ClientInit(format!(
1097 "Failed to load TLS root certificate {index}: {err}"
1098 ))
1099 })
1100 })
1101 .collect()
1102}
1103
1104#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1105struct FetchBody {
1106 url: String,
1107 reader: web_sys::ReadableStreamDefaultReader,
1108 control: HttpControl,
1109 transferred: std::cell::Cell<u64>,
1110 total: Option<u64>,
1111}
1112
1113#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1114impl HttpBody for FetchBody {
1115 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
1116 use wasm_bindgen_futures::JsFuture;
1117
1118 Box::pin(async move {
1119 if self.control.is_cancelled() {
1120 let _ = self.reader.cancel();
1121 return Err(HttpError::Cancelled);
1122 }
1123 let result = JsFuture::from(self.reader.read()).await.map_err(|error| {
1124 HttpError::BodyReadFailed {
1125 url: self.url.clone(),
1126 message: format!("{error:?}"),
1127 }
1128 })?;
1129 let done = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("done"))
1130 .ok()
1131 .and_then(|value| value.as_bool())
1132 .unwrap_or(true);
1133 if done {
1134 return Ok(None);
1135 }
1136 let value = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("value"))
1137 .map_err(|error| HttpError::BodyReadFailed {
1138 url: self.url.clone(),
1139 message: format!("{error:?}"),
1140 })?;
1141 let chunk = js_sys::Uint8Array::new(&value).to_vec();
1142 self.transferred
1143 .set(self.transferred.get() + chunk.len() as u64);
1144 self.control.report(HttpProgress {
1145 transferred: self.transferred.get(),
1146 total: self.total,
1147 });
1148 Ok(Some(chunk))
1149 })
1150 }
1151}
1152
1153#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1154async fn send_web(request: &HttpRequest, control: HttpControl) -> Result<HttpResponse, HttpError> {
1155 use wasm_bindgen::JsCast;
1156 use wasm_bindgen_futures::JsFuture;
1157 use web_sys::{Request, RequestInit, RequestMode, Response};
1158
1159 if control.is_cancelled() {
1160 return Err(HttpError::Cancelled);
1161 }
1162 let options = RequestInit::new();
1163 options.set_method(request.method.name());
1164 options.set_mode(RequestMode::Cors);
1165 if let Some(body) = &request.body {
1166 options.set_body(&js_sys::Uint8Array::from(body.as_slice()).into());
1167 }
1168
1169 let fetch_request =
1170 Request::new_with_str_and_init(&request.url, &options).map_err(|error| {
1171 HttpError::RequestFailed {
1172 url: request.url.clone(),
1173 message: format!("{error:?}"),
1174 }
1175 })?;
1176 let headers = fetch_request.headers();
1177 for (name, value) in &request.headers {
1178 headers
1179 .set(name, value)
1180 .map_err(|error| HttpError::RequestFailed {
1181 url: request.url.clone(),
1182 message: format!("{error:?}"),
1183 })?;
1184 }
1185 if let Some(offset) = request.resume_from {
1186 headers
1187 .set("Range", &format!("bytes={offset}-"))
1188 .map_err(|error| HttpError::RequestFailed {
1189 url: request.url.clone(),
1190 message: format!("{error:?}"),
1191 })?;
1192 }
1193
1194 let window = web_sys::window().ok_or(HttpError::NoWindow)?;
1195 let value = JsFuture::from(window.fetch_with_request(&fetch_request))
1196 .await
1197 .map_err(|error| HttpError::RequestFailed {
1198 url: request.url.clone(),
1199 message: format!("{error:?}"),
1200 })?;
1201 let response: Response = value.dyn_into().map_err(|_| HttpError::InvalidResponse {
1202 url: request.url.clone(),
1203 message: "the browser answered with something that is not a Response".to_string(),
1204 })?;
1205
1206 let status = response.status();
1207 let mut header_pairs = Vec::new();
1208 let entries = js_sys::try_iter(&response.headers()).ok().flatten();
1209 if let Some(entries) = entries {
1210 for entry in entries.flatten() {
1211 let pair = js_sys::Array::from(&entry);
1212 if pair.length() >= 2 {
1213 let name = pair.get(0).as_string().unwrap_or_default();
1214 let value = pair.get(1).as_string().unwrap_or_default();
1215 header_pairs.push((name.to_ascii_lowercase(), value));
1216 }
1217 }
1218 }
1219 let total = header_pairs
1220 .iter()
1221 .find(|(name, _)| name == "content-length")
1222 .and_then(|(_, value)| value.parse::<u64>().ok());
1223 let resumed = request.resume_from.is_some() && status == 206;
1224
1225 let body = response.body().ok_or_else(|| HttpError::InvalidResponse {
1226 url: request.url.clone(),
1227 message: "the response carries no body".to_string(),
1228 })?;
1229 let reader: web_sys::ReadableStreamDefaultReader =
1230 body.get_reader()
1231 .dyn_into()
1232 .map_err(|_| HttpError::InvalidResponse {
1233 url: request.url.clone(),
1234 message: "the response body cannot be read in chunks".to_string(),
1235 })?;
1236
1237 Ok(HttpResponse::new(
1238 request.url.clone(),
1239 status,
1240 http_body_ref(FetchBody {
1241 url: request.url.clone(),
1242 reader,
1243 control,
1244 transferred: std::cell::Cell::new(0),
1245 total,
1246 }),
1247 )
1248 .with_headers(header_pairs)
1249 .with_content_length(total)
1250 .with_resumed(resumed))
1251}
1252
1253#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
1254async fn send_web(request: &HttpRequest, _control: HttpControl) -> Result<HttpResponse, HttpError> {
1255 let _ = request;
1256 Err(HttpError::UnsupportedFeature {
1257 operation: "web HTTP requests",
1258 feature: "web-http",
1259 })
1260}
1261
1262pub fn default_http_client() -> HttpClientRef {
1270 Arc::new(DefaultHttpClient::new())
1271}
1272
1273pub fn local_http_client() -> CompositionLocal<HttpClientRef> {
1274 crate::composition_locals::cached_local(
1275 |locals| &locals.http,
1276 || compositionLocalOfWithPolicy(default_http_client, Arc::ptr_eq),
1277 )
1278}
1279
1280#[cfg(test)]
1281#[path = "tests/http_tests.rs"]
1282mod tests;