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)]
18pub enum HttpError {
19 #[error("Failed to build HTTP client: {0}")]
21 ClientInit(String),
22 #[error("Request failed for {url}: {message}")]
24 RequestFailed {
25 url: String,
27 message: String,
29 },
30 #[error("Request failed with status {status} for {url}")]
32 HttpStatus {
33 url: String,
35 status: u16,
37 },
38 #[error("Failed to read response body for {url}: {message}")]
40 BodyReadFailed {
41 url: String,
43 message: String,
45 },
46 #[error("Invalid response for {url}: {message}")]
48 InvalidResponse {
49 url: String,
51 message: String,
53 },
54 #[error("No window object available")]
56 NoWindow,
57 #[error("{operation} worker thread panicked")]
59 WorkerPanicked {
60 operation: &'static str,
62 },
63 #[error("{operation} requires cranpose-services feature `{feature}`")]
65 UnsupportedFeature {
66 operation: &'static str,
68 feature: &'static str,
70 },
71 #[error("Download was cancelled")]
73 Cancelled,
74 #[error("Failed to write downloaded file {path}: {message}")]
76 FileWrite {
77 path: String,
79 kind: std::io::ErrorKind,
82 message: String,
84 },
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub struct HttpProgress {
90 pub transferred: u64,
92 pub total: Option<u64>,
95}
96
97impl HttpProgress {
98 pub fn fraction(&self) -> Option<f32> {
101 let total = self.total?;
102 if total == 0 {
103 return None;
104 }
105 Some((self.transferred as f32 / total as f32).clamp(0.0, 1.0))
106 }
107
108 pub fn is_complete(&self) -> bool {
110 self.total.is_some_and(|total| self.transferred >= total)
111 }
112}
113
114pub type ProgressHandler = Arc<dyn Fn(HttpProgress) + Send + Sync>;
116
117#[derive(Clone, Default)]
123pub struct HttpControl {
124 cancel: Arc<CancelState>,
125 progress: Option<ProgressHandler>,
126}
127
128#[derive(Default)]
129struct CancelState {
130 cancelled: AtomicBool,
131 waiting: Mutex<Vec<Weak<dyn CancelTransfer>>>,
132}
133
134pub trait CancelTransfer: Send + Sync {
137 fn cancel(&self);
139}
140
141impl HttpControl {
142 pub fn new() -> Self {
144 Self::default()
145 }
146
147 pub fn with_progress(
153 mut self,
154 progress: impl Fn(HttpProgress) + Send + Sync + 'static,
155 ) -> Self {
156 self.progress = Some(Arc::new(progress));
157 self
158 }
159
160 pub fn cancel(&self) {
168 self.cancel.cancelled.store(true, Ordering::Release);
169 let waiting = std::mem::take(
170 &mut *self
171 .cancel
172 .waiting
173 .lock()
174 .unwrap_or_else(PoisonError::into_inner),
175 );
176 for transfer in waiting.iter().filter_map(Weak::upgrade) {
177 transfer.cancel();
178 }
179 }
180
181 #[cfg(not(target_arch = "wasm32"))]
183 fn cancel_only(&self) -> Self {
184 Self {
185 cancel: Arc::clone(&self.cancel),
186 progress: None,
187 }
188 }
189
190 pub fn is_cancelled(&self) -> bool {
192 self.cancel.cancelled.load(Ordering::Acquire)
193 }
194
195 pub fn on_cancel(&self, transfer: Weak<dyn CancelTransfer>) {
199 let mut waiting = self
200 .cancel
201 .waiting
202 .lock()
203 .unwrap_or_else(PoisonError::into_inner);
204 if self.is_cancelled() {
205 drop(waiting);
206 if let Some(transfer) = transfer.upgrade() {
207 transfer.cancel();
208 }
209 return;
210 }
211 waiting.retain(|transfer| transfer.strong_count() > 0);
212 waiting.push(transfer);
213 }
214
215 pub fn report(&self, progress: HttpProgress) {
223 if let Some(handler) = &self.progress {
224 handler(progress);
225 }
226 }
227}
228
229#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
231pub enum HttpMethod {
232 #[default]
234 Get,
235 Head,
237 Post,
239 Put,
241 Delete,
243 Propfind,
245}
246
247impl HttpMethod {
248 pub fn name(self) -> &'static str {
250 match self {
251 Self::Get => "GET",
252 Self::Head => "HEAD",
253 Self::Post => "POST",
254 Self::Put => "PUT",
255 Self::Delete => "DELETE",
256 Self::Propfind => "PROPFIND",
257 }
258 }
259}
260
261#[derive(Clone, Debug, Default, PartialEq, Eq)]
263pub struct HttpRequest {
264 pub url: String,
266 pub method: HttpMethod,
268 pub headers: Vec<(String, String)>,
270 pub body: Option<Vec<u8>>,
272 pub resume_from: Option<u64>,
279}
280
281impl HttpRequest {
282 pub fn get(url: impl Into<String>) -> Self {
284 Self {
285 url: url.into(),
286 ..Self::default()
287 }
288 }
289
290 pub fn method(mut self, method: HttpMethod) -> Self {
292 self.method = method;
293 self
294 }
295
296 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
298 self.headers.push((name.into(), value.into()));
299 self
300 }
301
302 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
304 self.body = Some(body.into());
305 self
306 }
307
308 pub fn resume_from(mut self, offset: u64) -> Self {
310 self.resume_from = (offset > 0).then_some(offset);
311 self
312 }
313}
314
315#[cfg(not(target_arch = "wasm32"))]
317pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + Send + 'a>>;
318
319#[cfg(target_arch = "wasm32")]
322pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + 'a>>;
323
324pub trait HttpBody {
331 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>>;
333}
334
335#[cfg(not(target_arch = "wasm32"))]
342pub type HttpBodyRef = Arc<dyn HttpBody + Send + Sync>;
343
344#[cfg(target_arch = "wasm32")]
350pub type HttpBodyRef = std::rc::Rc<dyn HttpBody>;
351
352#[cfg(not(target_arch = "wasm32"))]
359pub fn http_body_ref<B: HttpBody + Send + Sync + 'static>(body: B) -> HttpBodyRef {
360 Arc::new(body)
361}
362
363#[cfg(target_arch = "wasm32")]
365pub fn http_body_ref<B: HttpBody + 'static>(body: B) -> HttpBodyRef {
366 std::rc::Rc::new(body)
367}
368
369struct EmptyBody;
370
371impl HttpBody for EmptyBody {
372 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
373 Box::pin(async { Ok(None) })
374 }
375}
376
377pub struct BytesBody {
383 bytes: Vec<u8>,
384 offset: Mutex<usize>,
385 chunk: usize,
386}
387
388impl BytesBody {
389 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
391 Self::chunked(bytes, usize::MAX)
392 }
393
394 pub fn chunked(bytes: impl Into<Vec<u8>>, chunk: usize) -> Self {
396 Self {
397 bytes: bytes.into(),
398 offset: Mutex::new(0),
399 chunk: chunk.max(1),
400 }
401 }
402
403 pub fn len(&self) -> u64 {
405 self.bytes.len() as u64
406 }
407
408 pub fn is_empty(&self) -> bool {
410 self.bytes.is_empty()
411 }
412}
413
414impl HttpBody for BytesBody {
415 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
416 Box::pin(async move {
417 let mut offset = self.offset.lock().unwrap_or_else(PoisonError::into_inner);
418 if *offset >= self.bytes.len() {
419 return Ok(None);
420 }
421 let end = offset.saturating_add(self.chunk).min(self.bytes.len());
422 let chunk = self.bytes[*offset..end].to_vec();
423 *offset = end;
424 Ok(Some(chunk))
425 })
426 }
427}
428
429pub struct HttpResponse {
431 pub status: u16,
433 pub headers: Vec<(String, String)>,
435 pub content_length: Option<u64>,
437 pub resumed: bool,
442 pub url: String,
444 body: HttpBodyRef,
445}
446
447impl HttpResponse {
448 pub fn new(url: impl Into<String>, status: u16, body: HttpBodyRef) -> Self {
450 Self {
451 status,
452 headers: Vec::new(),
453 content_length: None,
454 resumed: false,
455 url: url.into(),
456 body,
457 }
458 }
459
460 pub fn empty(url: impl Into<String>, status: u16) -> Self {
462 Self::new(url, status, http_body_ref(EmptyBody))
463 }
464
465 pub fn with_headers(mut self, headers: Vec<(String, String)>) -> Self {
467 self.headers = headers;
468 self
469 }
470
471 pub fn with_content_length(mut self, content_length: Option<u64>) -> Self {
473 self.content_length = content_length;
474 self
475 }
476
477 pub fn with_resumed(mut self, resumed: bool) -> Self {
479 self.resumed = resumed;
480 self
481 }
482
483 pub fn is_success(&self) -> bool {
485 (200..300).contains(&self.status)
486 }
487
488 pub fn header(&self, name: &str) -> Option<&str> {
490 let name = name.to_ascii_lowercase();
491 self.headers
492 .iter()
493 .find(|(key, _)| key.eq_ignore_ascii_case(&name))
494 .map(|(_, value)| value.as_str())
495 }
496
497 pub fn error_for_status(self) -> Result<Self, HttpError> {
499 if self.is_success() {
500 Ok(self)
501 } else {
502 Err(HttpError::HttpStatus {
503 url: self.url.clone(),
504 status: self.status,
505 })
506 }
507 }
508
509 pub async fn read_chunk(&self) -> Result<Option<Vec<u8>>, HttpError> {
511 self.body.read_chunk().await
512 }
513
514 pub async fn read_all(&self) -> Result<Vec<u8>, HttpError> {
519 let mut out = Vec::with_capacity(self.content_length.unwrap_or(0).min(1 << 20) as usize);
520 while let Some(chunk) = self.read_chunk().await? {
521 out.extend_from_slice(&chunk);
522 }
523 Ok(out)
524 }
525
526 pub async fn read_text(&self) -> Result<String, HttpError> {
528 String::from_utf8(self.read_all().await?).map_err(|error| HttpError::InvalidResponse {
529 url: self.url.clone(),
530 message: error.to_string(),
531 })
532 }
533}
534
535pub trait HttpClient: Send + Sync {
542 fn send<'a>(
545 &'a self,
546 request: &'a HttpRequest,
547 control: HttpControl,
548 ) -> HttpFuture<'a, HttpResponse>;
549
550 fn get_text<'a>(&'a self, url: &'a str) -> HttpFuture<'a, String> {
552 Box::pin(async move {
553 self.send(&HttpRequest::get(url), HttpControl::new())
554 .await?
555 .error_for_status()?
556 .read_text()
557 .await
558 })
559 }
560
561 fn get_bytes<'a>(&'a self, url: &'a str) -> HttpFuture<'a, Vec<u8>> {
563 Box::pin(async move {
564 self.send(&HttpRequest::get(url), HttpControl::new())
565 .await?
566 .error_for_status()?
567 .read_all()
568 .await
569 })
570 }
571
572 #[cfg(not(target_arch = "wasm32"))]
580 fn download_to<'a>(
581 &'a self,
582 url: &'a str,
583 target: &'a Path,
584 control: HttpControl,
585 ) -> HttpFuture<'a, u64> {
586 Box::pin(async move { download_through(self, url, target, control).await })
587 }
588}
589
590#[cfg(not(target_arch = "wasm32"))]
599pub trait PlatformDownloader: Send + Sync {
600 fn download<'a>(
604 &'a self,
605 url: &'a str,
606 target: &'a Path,
607 control: HttpControl,
608 ) -> HttpFuture<'a, u64>;
609
610 fn downloaded_length(&self, target: &Path) -> Option<u64>;
614}
615
616#[cfg(not(target_arch = "wasm32"))]
621pub fn downloaded_length(target: &Path) -> u64 {
622 PLATFORM_DOWNLOADER
623 .get()
624 .and_then(|downloader| downloader.downloaded_length(target))
625 .unwrap_or_else(|| std::fs::metadata(target).map_or(0, |metadata| metadata.len()))
626}
627
628#[cfg(not(target_arch = "wasm32"))]
629static PLATFORM_DOWNLOADER: crate::registry::ServiceRegistry<dyn PlatformDownloader> =
630 crate::registry::ServiceRegistry::new();
631
632#[cfg(not(target_arch = "wasm32"))]
637pub fn set_platform_downloader(downloader: Arc<dyn PlatformDownloader>) {
638 PLATFORM_DOWNLOADER.set(downloader);
639}
640
641#[cfg(not(target_arch = "wasm32"))]
642async fn download_through<C: HttpClient + ?Sized>(
643 client: &C,
644 url: &str,
645 target: &Path,
646 control: HttpControl,
647) -> Result<u64, HttpError> {
648 use std::io::Write;
649
650 if control.is_cancelled() {
651 return Err(HttpError::Cancelled);
652 }
653 let existing = std::fs::metadata(target).map_or(0, |metadata| metadata.len());
654 let mut request = HttpRequest::get(url);
655 if existing > 0 {
656 request = request.resume_from(existing);
657 }
658 let response = client
661 .send(&request, control.cancel_only())
662 .await?
663 .error_for_status()?;
664
665 let base = if response.resumed { existing } else { 0 };
666 let total = response.content_length.map(|remaining| base + remaining);
667 let mut output = if response.resumed {
668 std::fs::OpenOptions::new().append(true).open(target)
669 } else {
670 std::fs::File::create(target)
671 }
672 .map_err(|error| file_write_error(target, &error))?;
673
674 let mut transferred = base;
675 control.report(HttpProgress { transferred, total });
676 while let Some(chunk) = response.read_chunk().await? {
677 if control.is_cancelled() {
678 return Err(HttpError::Cancelled);
679 }
680 output
681 .write_all(&chunk)
682 .map_err(|error| file_write_error(target, &error))?;
683 transferred += chunk.len() as u64;
684 control.report(HttpProgress { transferred, total });
685 }
686 output
687 .flush()
688 .map_err(|error| file_write_error(target, &error))?;
689 Ok(transferred)
690}
691
692#[cfg(not(target_arch = "wasm32"))]
693fn file_write_error(target: &Path, error: &std::io::Error) -> HttpError {
694 HttpError::FileWrite {
695 path: target.display().to_string(),
696 kind: error.kind(),
697 message: error.to_string(),
698 }
699}
700
701pub type HttpClientRef = Arc<dyn HttpClient>;
703
704#[cfg(not(target_arch = "wasm32"))]
708pub async fn map_ordered_concurrent<I, T, F, Fut>(
709 items: &[I],
710 concurrency: usize,
711 task: F,
712) -> Result<Vec<T>, HttpError>
713where
714 I: Clone + Send,
715 T: Send,
716 F: Fn(I) -> Fut + Send + Sync + 'static,
717 Fut: Future<Output = T> + Send,
718{
719 let task = Arc::new(task);
720 let mut results = Vec::with_capacity(items.len());
721
722 for chunk in items.chunks(concurrency.max(1)) {
723 let chunk_results = std::thread::scope(|scope| {
724 let mut handles = Vec::with_capacity(chunk.len());
725 for item in chunk.iter().cloned() {
726 let task = Arc::clone(&task);
727 handles.push(scope.spawn(move || pollster::block_on(task(item))));
728 }
729
730 let mut chunk_results = Vec::with_capacity(handles.len());
731 for handle in handles {
732 let value = handle.join().map_err(|_| HttpError::WorkerPanicked {
733 operation: "ordered concurrent task",
734 })?;
735 chunk_results.push(value);
736 }
737 Ok::<Vec<T>, HttpError>(chunk_results)
738 })?;
739 results.extend(chunk_results);
740 }
741
742 Ok(results)
743}
744
745#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
748pub async fn map_ordered_concurrent<I, T, F, Fut>(
749 items: &[I],
750 concurrency: usize,
751 task: F,
752) -> Result<Vec<T>, HttpError>
753where
754 I: Clone,
755 F: Fn(I) -> Fut + Clone,
756 Fut: Future<Output = T>,
757{
758 let mut results = stream::iter(items.iter().cloned().enumerate().map(|(index, item)| {
759 let task = task.clone();
760 async move { (index, task(item).await) }
761 }))
762 .buffer_unordered(concurrency.max(1))
763 .collect::<Vec<_>>()
764 .await;
765
766 results.sort_by_key(|(index, _)| *index);
767 Ok(results.into_iter().map(|(_, value)| value).collect())
768}
769
770#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
773pub async fn map_ordered_concurrent<I, T, F, Fut>(
774 items: &[I],
775 _concurrency: usize,
776 task: F,
777) -> Result<Vec<T>, HttpError>
778where
779 I: Clone,
780 F: Fn(I) -> Fut,
781 Fut: Future<Output = T>,
782{
783 Ok(map_ordered_sequential(items, task).await)
784}
785
786#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
787async fn map_ordered_sequential<I, T, F, Fut>(items: &[I], task: F) -> Vec<T>
788where
789 I: Clone,
790 F: Fn(I) -> Fut,
791 Fut: Future<Output = T>,
792{
793 let mut results = Vec::with_capacity(items.len());
794 for item in items.iter().cloned() {
795 results.push(task(item).await);
796 }
797 results
798}
799
800pub type StubAnswer = dyn Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync;
802
803pub struct StubHttpClient {
817 answer: Box<StubAnswer>,
818}
819
820impl StubHttpClient {
821 pub fn new(
823 answer: impl Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync + 'static,
824 ) -> Self {
825 Self {
826 answer: Box::new(answer),
827 }
828 }
829
830 pub fn with_body(body: impl Into<Vec<u8>>) -> Self {
832 let body = body.into();
833 Self::new(move |request| {
834 Ok(HttpResponse::new(
835 request.url.clone(),
836 200,
837 http_body_ref(BytesBody::new(body.clone())),
838 ))
839 })
840 }
841
842 pub fn from_text(
844 answer: impl Fn(&str) -> Result<String, HttpError> + Send + Sync + 'static,
845 ) -> Self {
846 Self::new(move |request| {
847 let body = answer(&request.url)?;
848 Ok(HttpResponse::new(
849 request.url.clone(),
850 200,
851 http_body_ref(BytesBody::new(body)),
852 ))
853 })
854 }
855}
856
857impl HttpClient for StubHttpClient {
858 fn send<'a>(
859 &'a self,
860 request: &'a HttpRequest,
861 _control: HttpControl,
862 ) -> HttpFuture<'a, HttpResponse> {
863 Box::pin(async move { (self.answer)(request) })
864 }
865}
866
867#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
868const CHUNK_LEN: usize = 64 * 1024;
869
870#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
871const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
872
873struct DefaultHttpClient {
874 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
875 native_client: Result<reqwest::blocking::Client, HttpError>,
876}
877
878impl DefaultHttpClient {
879 fn new() -> Self {
880 Self {
881 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
882 native_client: build_native_client(IDLE_TIMEOUT),
883 }
884 }
885
886 #[cfg(all(test, not(target_arch = "wasm32"), feature = "http-native"))]
887 fn with_idle_timeout(idle: std::time::Duration) -> Self {
888 Self {
889 native_client: build_native_client(idle),
890 }
891 }
892}
893
894#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
895struct ResponseHead {
896 status: u16,
897 headers: Vec<(String, String)>,
898 content_length: Option<u64>,
899 resumed: bool,
900}
901
902#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
903struct NativeTransfer {
904 head: crate::async_io::Signal<Result<ResponseHead, HttpError>>,
905 chunks: crate::async_io::ChunkStream<HttpError>,
906}
907
908#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
909impl CancelTransfer for NativeTransfer {
910 fn cancel(&self) {
911 self.head.set(Err(HttpError::Cancelled));
912 self.chunks.fail(HttpError::Cancelled);
913 }
914}
915
916#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
917struct ChannelBody {
918 transfer: Arc<NativeTransfer>,
919}
920
921#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
922impl HttpBody for ChannelBody {
923 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
924 Box::pin(self.transfer.chunks.next())
925 }
926}
927
928#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
929async fn send_native(
930 client: reqwest::blocking::Client,
931 request: HttpRequest,
932 control: HttpControl,
933) -> Result<HttpResponse, HttpError> {
934 use crate::async_io::{ChunkChannel, Signal};
935
936 let head = Signal::new();
937 let (chunks, stream) = ChunkChannel::<HttpError>::new();
938 let transfer = Arc::new(NativeTransfer {
939 head: head.clone(),
940 chunks: stream,
941 });
942 control.on_cancel(Arc::<NativeTransfer>::downgrade(&transfer));
943 let url = request.url.clone();
944
945 std::thread::Builder::new()
946 .name("cranpose-http".to_string())
947 .spawn(move || {
948 if let Err(error) = read_native_response(&client, request, &control, &chunks, &head) {
949 head.set(Err(error.clone()));
950 chunks.fail(error);
951 }
952 })
953 .map_err(|error| HttpError::RequestFailed {
954 url: url.clone(),
955 message: format!("could not start the transfer: {error}"),
956 })?;
957
958 let head = transfer
959 .head
960 .wait()
961 .await
962 .ok_or_else(|| HttpError::RequestFailed {
963 url: url.clone(),
964 message: "the transfer ended before a response arrived".to_string(),
965 })??;
966
967 Ok(
968 HttpResponse::new(url, head.status, http_body_ref(ChannelBody { transfer }))
969 .with_headers(head.headers)
970 .with_content_length(head.content_length)
971 .with_resumed(head.resumed),
972 )
973}
974
975#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
976fn read_native_response(
977 client: &reqwest::blocking::Client,
978 request: HttpRequest,
979 control: &HttpControl,
980 chunks: &crate::async_io::ChunkChannel<HttpError>,
981 head_signal: &crate::async_io::Signal<Result<ResponseHead, HttpError>>,
982) -> Result<(), HttpError> {
983 use std::io::Read;
984
985 if control.is_cancelled() {
986 return Err(HttpError::Cancelled);
987 }
988 let HttpRequest {
989 url,
990 method,
991 headers,
992 body,
993 resume_from,
994 } = request;
995 let method = reqwest::Method::from_bytes(method.name().as_bytes()).expect("valid HTTP method");
996 let mut builder = client.request(method, &url);
997 for (name, value) in headers {
998 builder = builder.header(name, value);
999 }
1000 if let Some(offset) = resume_from {
1001 builder = builder.header(reqwest::header::RANGE, format!("bytes={offset}-"));
1002 }
1003 if let Some(body) = body {
1004 builder = builder.body(body);
1005 }
1006
1007 let mut response = builder.send().map_err(|error| HttpError::RequestFailed {
1008 url: url.clone(),
1009 message: error.to_string(),
1010 })?;
1011
1012 let status = response.status();
1013 let resumed = resume_from.is_some() && status == reqwest::StatusCode::PARTIAL_CONTENT;
1014 let content_length = response.content_length();
1015 let headers = response
1016 .headers()
1017 .iter()
1018 .map(|(name, value)| {
1019 (
1020 name.as_str().to_ascii_lowercase(),
1021 value.to_str().unwrap_or_default().to_string(),
1022 )
1023 })
1024 .collect();
1025 head_signal.set(Ok(ResponseHead {
1026 status: status.as_u16(),
1027 headers,
1028 content_length,
1029 resumed,
1030 }));
1031
1032 let mut buffer = vec![0u8; CHUNK_LEN];
1033 let mut transferred = 0u64;
1034 loop {
1035 if control.is_cancelled() {
1036 return Err(HttpError::Cancelled);
1037 }
1038 let count = response
1039 .read(&mut buffer)
1040 .map_err(|error| HttpError::BodyReadFailed {
1041 url: url.clone(),
1042 message: error.to_string(),
1043 })?;
1044 if count == 0 {
1045 break;
1046 }
1047 transferred += count as u64;
1048 control.report(HttpProgress {
1049 transferred,
1050 total: content_length,
1051 });
1052 if !chunks.push(buffer[..count].to_vec()) {
1053 break;
1054 }
1055 }
1056 Ok(())
1057}
1058
1059#[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
1060async fn send_native(
1061 _request: HttpRequest,
1062 _control: HttpControl,
1063) -> Result<HttpResponse, HttpError> {
1064 Err(HttpError::UnsupportedFeature {
1065 operation: "native HTTP requests",
1066 feature: "http-native",
1067 })
1068}
1069
1070impl HttpClient for DefaultHttpClient {
1071 fn send<'a>(
1072 &'a self,
1073 request: &'a HttpRequest,
1074 control: HttpControl,
1075 ) -> HttpFuture<'a, HttpResponse> {
1076 Box::pin(async move {
1077 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1078 {
1079 let client = self.native_client.as_ref().map_err(Clone::clone)?.clone();
1080 send_native(client, request.clone(), control).await
1081 }
1082 #[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
1083 {
1084 send_native(request.clone(), control).await
1085 }
1086 #[cfg(target_arch = "wasm32")]
1087 {
1088 send_web(request, control).await
1089 }
1090 })
1091 }
1092
1093 #[cfg(not(target_arch = "wasm32"))]
1094 fn download_to<'a>(
1095 &'a self,
1096 url: &'a str,
1097 target: &'a Path,
1098 control: HttpControl,
1099 ) -> HttpFuture<'a, u64> {
1100 Box::pin(async move {
1101 match PLATFORM_DOWNLOADER.get() {
1102 Some(downloader) => downloader.download(url, target, control).await,
1103 None => download_through(self, url, target, control).await,
1104 }
1105 })
1106 }
1107}
1108
1109#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1110fn build_native_client(idle: std::time::Duration) -> Result<reqwest::blocking::Client, HttpError> {
1111 configure_native_client_builder(
1112 reqwest::blocking::Client::builder()
1113 .connect_timeout(std::time::Duration::from_secs(30))
1114 .timeout(idle)
1115 .user_agent(concat!("cranpose/", env!("CARGO_PKG_VERSION"))),
1116 )?
1117 .build()
1118 .map_err(|err| HttpError::ClientInit(err.to_string()))
1119}
1120
1121#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1122fn configure_native_client_builder(
1123 builder: reqwest::blocking::ClientBuilder,
1124) -> Result<reqwest::blocking::ClientBuilder, HttpError> {
1125 #[cfg(target_os = "android")]
1126 {
1127 Ok(builder.tls_certs_only(android_root_certificates()?))
1128 }
1129
1130 #[cfg(not(target_os = "android"))]
1131 {
1132 Ok(builder)
1133 }
1134}
1135
1136#[cfg(all(target_os = "android", feature = "http-native"))]
1137fn android_root_certificates() -> Result<Vec<reqwest::Certificate>, HttpError> {
1138 certificates_from_der_chain(
1139 webpki_root_certs::TLS_SERVER_ROOT_CERTS
1140 .iter()
1141 .map(AsRef::as_ref),
1142 )
1143}
1144
1145#[cfg(any(
1146 all(test, not(target_arch = "wasm32"), feature = "http-native"),
1147 all(target_os = "android", feature = "http-native")
1148))]
1149fn certificates_from_der_chain<'a, I>(
1150 certificates: I,
1151) -> Result<Vec<reqwest::Certificate>, HttpError>
1152where
1153 I: IntoIterator<Item = &'a [u8]>,
1154{
1155 certificates
1156 .into_iter()
1157 .enumerate()
1158 .map(|(index, der)| {
1159 reqwest::Certificate::from_der(der).map_err(|err| {
1160 HttpError::ClientInit(format!(
1161 "Failed to load TLS root certificate {index}: {err}"
1162 ))
1163 })
1164 })
1165 .collect()
1166}
1167
1168#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1169struct FetchBody {
1170 url: String,
1171 reader: web_sys::ReadableStreamDefaultReader,
1172 control: HttpControl,
1173 transferred: std::cell::Cell<u64>,
1174 total: Option<u64>,
1175}
1176
1177#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1178impl HttpBody for FetchBody {
1179 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
1180 use wasm_bindgen_futures::JsFuture;
1181
1182 Box::pin(async move {
1183 if self.control.is_cancelled() {
1184 let _ = self.reader.cancel();
1185 return Err(HttpError::Cancelled);
1186 }
1187 let result = JsFuture::from(self.reader.read()).await.map_err(|error| {
1188 HttpError::BodyReadFailed {
1189 url: self.url.clone(),
1190 message: format!("{error:?}"),
1191 }
1192 })?;
1193 let done = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("done"))
1194 .ok()
1195 .and_then(|value| value.as_bool())
1196 .unwrap_or(true);
1197 if done {
1198 return Ok(None);
1199 }
1200 let value = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("value"))
1201 .map_err(|error| HttpError::BodyReadFailed {
1202 url: self.url.clone(),
1203 message: format!("{error:?}"),
1204 })?;
1205 let chunk = js_sys::Uint8Array::new(&value).to_vec();
1206 self.transferred
1207 .set(self.transferred.get() + chunk.len() as u64);
1208 self.control.report(HttpProgress {
1209 transferred: self.transferred.get(),
1210 total: self.total,
1211 });
1212 Ok(Some(chunk))
1213 })
1214 }
1215}
1216
1217#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1218async fn send_web(request: &HttpRequest, control: HttpControl) -> Result<HttpResponse, HttpError> {
1219 use wasm_bindgen::JsCast;
1220 use wasm_bindgen_futures::JsFuture;
1221 use web_sys::{Request, RequestInit, RequestMode, Response};
1222
1223 if control.is_cancelled() {
1224 return Err(HttpError::Cancelled);
1225 }
1226 let options = RequestInit::new();
1227 options.set_method(request.method.name());
1228 options.set_mode(RequestMode::Cors);
1229 if let Some(body) = &request.body {
1230 options.set_body(&js_sys::Uint8Array::from(body.as_slice()).into());
1231 }
1232
1233 let fetch_request =
1234 Request::new_with_str_and_init(&request.url, &options).map_err(|error| {
1235 HttpError::RequestFailed {
1236 url: request.url.clone(),
1237 message: format!("{error:?}"),
1238 }
1239 })?;
1240 let headers = fetch_request.headers();
1241 for (name, value) in &request.headers {
1242 headers
1243 .set(name, value)
1244 .map_err(|error| HttpError::RequestFailed {
1245 url: request.url.clone(),
1246 message: format!("{error:?}"),
1247 })?;
1248 }
1249 if let Some(offset) = request.resume_from {
1250 headers
1251 .set("Range", &format!("bytes={offset}-"))
1252 .map_err(|error| HttpError::RequestFailed {
1253 url: request.url.clone(),
1254 message: format!("{error:?}"),
1255 })?;
1256 }
1257
1258 let window = web_sys::window().ok_or(HttpError::NoWindow)?;
1259 let value = JsFuture::from(window.fetch_with_request(&fetch_request))
1260 .await
1261 .map_err(|error| HttpError::RequestFailed {
1262 url: request.url.clone(),
1263 message: format!("{error:?}"),
1264 })?;
1265 let response: Response = value.dyn_into().map_err(|_| HttpError::InvalidResponse {
1266 url: request.url.clone(),
1267 message: "the browser answered with something that is not a Response".to_string(),
1268 })?;
1269
1270 let status = response.status();
1271 let mut header_pairs = Vec::new();
1272 let entries = js_sys::try_iter(&response.headers()).ok().flatten();
1273 if let Some(entries) = entries {
1274 for entry in entries.flatten() {
1275 let pair = js_sys::Array::from(&entry);
1276 if pair.length() >= 2 {
1277 let name = pair.get(0).as_string().unwrap_or_default();
1278 let value = pair.get(1).as_string().unwrap_or_default();
1279 header_pairs.push((name.to_ascii_lowercase(), value));
1280 }
1281 }
1282 }
1283 let total = header_pairs
1284 .iter()
1285 .find(|(name, _)| name == "content-length")
1286 .and_then(|(_, value)| value.parse::<u64>().ok());
1287 let resumed = request.resume_from.is_some() && status == 206;
1288
1289 let body = response.body().ok_or_else(|| HttpError::InvalidResponse {
1290 url: request.url.clone(),
1291 message: "the response carries no body".to_string(),
1292 })?;
1293 let reader: web_sys::ReadableStreamDefaultReader =
1294 body.get_reader()
1295 .dyn_into()
1296 .map_err(|_| HttpError::InvalidResponse {
1297 url: request.url.clone(),
1298 message: "the response body cannot be read in chunks".to_string(),
1299 })?;
1300
1301 Ok(HttpResponse::new(
1302 request.url.clone(),
1303 status,
1304 http_body_ref(FetchBody {
1305 url: request.url.clone(),
1306 reader,
1307 control,
1308 transferred: std::cell::Cell::new(0),
1309 total,
1310 }),
1311 )
1312 .with_headers(header_pairs)
1313 .with_content_length(total)
1314 .with_resumed(resumed))
1315}
1316
1317#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
1318async fn send_web(request: &HttpRequest, _control: HttpControl) -> Result<HttpResponse, HttpError> {
1319 let _ = request;
1320 Err(HttpError::UnsupportedFeature {
1321 operation: "web HTTP requests",
1322 feature: "web-http",
1323 })
1324}
1325
1326pub fn default_http_client() -> HttpClientRef {
1334 Arc::new(DefaultHttpClient::new())
1335}
1336
1337pub fn local_http_client() -> CompositionLocal<HttpClientRef> {
1340 crate::composition_locals::cached_local(
1341 |locals| &locals.http,
1342 || compositionLocalOfWithPolicy(default_http_client, Arc::ptr_eq),
1343 )
1344}
1345
1346#[cfg(test)]
1347#[path = "tests/http_tests.rs"]
1348mod tests;