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}
244
245impl HttpMethod {
246 pub fn name(self) -> &'static str {
248 match self {
249 HttpMethod::Get => "GET",
250 HttpMethod::Head => "HEAD",
251 HttpMethod::Post => "POST",
252 HttpMethod::Put => "PUT",
253 HttpMethod::Delete => "DELETE",
254 }
255 }
256}
257
258#[derive(Clone, Debug, Default, PartialEq, Eq)]
260pub struct HttpRequest {
261 pub url: String,
263 pub method: HttpMethod,
265 pub headers: Vec<(String, String)>,
267 pub body: Option<Vec<u8>>,
269 pub resume_from: Option<u64>,
276}
277
278impl HttpRequest {
279 pub fn get(url: impl Into<String>) -> Self {
281 Self {
282 url: url.into(),
283 ..Self::default()
284 }
285 }
286
287 pub fn method(mut self, method: HttpMethod) -> Self {
289 self.method = method;
290 self
291 }
292
293 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
295 self.headers.push((name.into(), value.into()));
296 self
297 }
298
299 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
301 self.body = Some(body.into());
302 self
303 }
304
305 pub fn resume_from(mut self, offset: u64) -> Self {
307 self.resume_from = (offset > 0).then_some(offset);
308 self
309 }
310}
311
312#[cfg(not(target_arch = "wasm32"))]
314pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + Send + 'a>>;
315
316#[cfg(target_arch = "wasm32")]
319pub type HttpFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, HttpError>> + 'a>>;
320
321pub trait HttpBody {
328 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>>;
330}
331
332#[cfg(not(target_arch = "wasm32"))]
339pub type HttpBodyRef = Arc<dyn HttpBody + Send + Sync>;
340
341#[cfg(target_arch = "wasm32")]
347pub type HttpBodyRef = std::rc::Rc<dyn HttpBody>;
348
349#[cfg(not(target_arch = "wasm32"))]
356pub fn http_body_ref<B: HttpBody + Send + Sync + 'static>(body: B) -> HttpBodyRef {
357 Arc::new(body)
358}
359
360#[cfg(target_arch = "wasm32")]
362pub fn http_body_ref<B: HttpBody + 'static>(body: B) -> HttpBodyRef {
363 std::rc::Rc::new(body)
364}
365
366struct EmptyBody;
367
368impl HttpBody for EmptyBody {
369 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
370 Box::pin(async { Ok(None) })
371 }
372}
373
374pub struct BytesBody {
380 bytes: Vec<u8>,
381 offset: std::sync::Mutex<usize>,
382 chunk: usize,
383}
384
385impl BytesBody {
386 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
388 Self::chunked(bytes, usize::MAX)
389 }
390
391 pub fn chunked(bytes: impl Into<Vec<u8>>, chunk: usize) -> Self {
393 Self {
394 bytes: bytes.into(),
395 offset: std::sync::Mutex::new(0),
396 chunk: chunk.max(1),
397 }
398 }
399
400 pub fn len(&self) -> u64 {
402 self.bytes.len() as u64
403 }
404
405 pub fn is_empty(&self) -> bool {
407 self.bytes.is_empty()
408 }
409}
410
411impl HttpBody for BytesBody {
412 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
413 Box::pin(async move {
414 let mut offset = self.offset.lock().unwrap_or_else(PoisonError::into_inner);
415 if *offset >= self.bytes.len() {
416 return Ok(None);
417 }
418 let end = offset.saturating_add(self.chunk).min(self.bytes.len());
419 let chunk = self.bytes[*offset..end].to_vec();
420 *offset = end;
421 Ok(Some(chunk))
422 })
423 }
424}
425
426pub struct HttpResponse {
428 pub status: u16,
430 pub headers: Vec<(String, String)>,
432 pub content_length: Option<u64>,
434 pub resumed: bool,
439 pub url: String,
441 body: HttpBodyRef,
442}
443
444impl HttpResponse {
445 pub fn new(url: impl Into<String>, status: u16, body: HttpBodyRef) -> Self {
447 Self {
448 status,
449 headers: Vec::new(),
450 content_length: None,
451 resumed: false,
452 url: url.into(),
453 body,
454 }
455 }
456
457 pub fn empty(url: impl Into<String>, status: u16) -> Self {
459 Self::new(url, status, http_body_ref(EmptyBody))
460 }
461
462 pub fn with_headers(mut self, headers: Vec<(String, String)>) -> Self {
464 self.headers = headers;
465 self
466 }
467
468 pub fn with_content_length(mut self, content_length: Option<u64>) -> Self {
470 self.content_length = content_length;
471 self
472 }
473
474 pub fn with_resumed(mut self, resumed: bool) -> Self {
476 self.resumed = resumed;
477 self
478 }
479
480 pub fn is_success(&self) -> bool {
482 (200..300).contains(&self.status)
483 }
484
485 pub fn header(&self, name: &str) -> Option<&str> {
487 let name = name.to_ascii_lowercase();
488 self.headers
489 .iter()
490 .find(|(key, _)| key.eq_ignore_ascii_case(&name))
491 .map(|(_, value)| value.as_str())
492 }
493
494 pub fn error_for_status(self) -> Result<Self, HttpError> {
496 if self.is_success() {
497 Ok(self)
498 } else {
499 Err(HttpError::HttpStatus {
500 url: self.url.clone(),
501 status: self.status,
502 })
503 }
504 }
505
506 pub async fn read_chunk(&self) -> Result<Option<Vec<u8>>, HttpError> {
508 self.body.read_chunk().await
509 }
510
511 pub async fn read_all(&self) -> Result<Vec<u8>, HttpError> {
516 let mut out = Vec::with_capacity(self.content_length.unwrap_or(0).min(1 << 20) as usize);
517 while let Some(chunk) = self.read_chunk().await? {
518 out.extend_from_slice(&chunk);
519 }
520 Ok(out)
521 }
522
523 pub async fn read_text(&self) -> Result<String, HttpError> {
525 String::from_utf8(self.read_all().await?).map_err(|error| HttpError::InvalidResponse {
526 url: self.url.clone(),
527 message: error.to_string(),
528 })
529 }
530}
531
532pub trait HttpClient: Send + Sync {
539 fn send<'a>(
542 &'a self,
543 request: &'a HttpRequest,
544 control: HttpControl,
545 ) -> HttpFuture<'a, HttpResponse>;
546
547 fn get_text<'a>(&'a self, url: &'a str) -> HttpFuture<'a, String> {
549 Box::pin(async move {
550 self.send(&HttpRequest::get(url), HttpControl::new())
551 .await?
552 .error_for_status()?
553 .read_text()
554 .await
555 })
556 }
557
558 fn get_bytes<'a>(&'a self, url: &'a str) -> HttpFuture<'a, Vec<u8>> {
560 Box::pin(async move {
561 self.send(&HttpRequest::get(url), HttpControl::new())
562 .await?
563 .error_for_status()?
564 .read_all()
565 .await
566 })
567 }
568
569 #[cfg(not(target_arch = "wasm32"))]
577 fn download_to<'a>(
578 &'a self,
579 url: &'a str,
580 target: &'a Path,
581 control: HttpControl,
582 ) -> HttpFuture<'a, u64> {
583 Box::pin(async move { download_through(self, url, target, control).await })
584 }
585}
586
587#[cfg(not(target_arch = "wasm32"))]
596pub trait PlatformDownloader: Send + Sync {
597 fn download<'a>(
601 &'a self,
602 url: &'a str,
603 target: &'a Path,
604 control: HttpControl,
605 ) -> HttpFuture<'a, u64>;
606
607 fn downloaded_length(&self, target: &Path) -> Option<u64>;
611}
612
613#[cfg(not(target_arch = "wasm32"))]
618pub fn downloaded_length(target: &Path) -> u64 {
619 PLATFORM_DOWNLOADER
620 .get()
621 .and_then(|downloader| downloader.downloaded_length(target))
622 .unwrap_or_else(|| std::fs::metadata(target).map_or(0, |metadata| metadata.len()))
623}
624
625#[cfg(not(target_arch = "wasm32"))]
626static PLATFORM_DOWNLOADER: crate::registry::ServiceRegistry<dyn PlatformDownloader> =
627 crate::registry::ServiceRegistry::new();
628
629#[cfg(not(target_arch = "wasm32"))]
634pub fn set_platform_downloader(downloader: Arc<dyn PlatformDownloader>) {
635 PLATFORM_DOWNLOADER.set(downloader);
636}
637
638#[cfg(not(target_arch = "wasm32"))]
639async fn download_through<C: HttpClient + ?Sized>(
640 client: &C,
641 url: &str,
642 target: &Path,
643 control: HttpControl,
644) -> Result<u64, HttpError> {
645 use std::io::Write;
646
647 if control.is_cancelled() {
648 return Err(HttpError::Cancelled);
649 }
650 let existing = std::fs::metadata(target).map_or(0, |metadata| metadata.len());
651 let mut request = HttpRequest::get(url);
652 if existing > 0 {
653 request = request.resume_from(existing);
654 }
655 let response = client
658 .send(&request, control.cancel_only())
659 .await?
660 .error_for_status()?;
661
662 let base = if response.resumed { existing } else { 0 };
663 let total = response.content_length.map(|remaining| base + remaining);
664 let mut output = if response.resumed {
665 std::fs::OpenOptions::new().append(true).open(target)
666 } else {
667 std::fs::File::create(target)
668 }
669 .map_err(|error| file_write_error(target, &error))?;
670
671 let mut transferred = base;
672 control.report(HttpProgress { transferred, total });
673 while let Some(chunk) = response.read_chunk().await? {
674 if control.is_cancelled() {
675 return Err(HttpError::Cancelled);
676 }
677 output
678 .write_all(&chunk)
679 .map_err(|error| file_write_error(target, &error))?;
680 transferred += chunk.len() as u64;
681 control.report(HttpProgress { transferred, total });
682 }
683 output
684 .flush()
685 .map_err(|error| file_write_error(target, &error))?;
686 Ok(transferred)
687}
688
689#[cfg(not(target_arch = "wasm32"))]
690fn file_write_error(target: &Path, error: &std::io::Error) -> HttpError {
691 HttpError::FileWrite {
692 path: target.display().to_string(),
693 kind: error.kind(),
694 message: error.to_string(),
695 }
696}
697
698pub type HttpClientRef = Arc<dyn HttpClient>;
700
701#[cfg(not(target_arch = "wasm32"))]
705pub async fn map_ordered_concurrent<I, T, F, Fut>(
706 items: &[I],
707 concurrency: usize,
708 task: F,
709) -> Result<Vec<T>, HttpError>
710where
711 I: Clone + Send,
712 T: Send,
713 F: Fn(I) -> Fut + Send + Sync + 'static,
714 Fut: Future<Output = T> + Send,
715{
716 let task = Arc::new(task);
717 let mut results = Vec::with_capacity(items.len());
718
719 for chunk in items.chunks(concurrency.max(1)) {
720 let chunk_results = std::thread::scope(|scope| {
721 let mut handles = Vec::with_capacity(chunk.len());
722 for item in chunk.iter().cloned() {
723 let task = Arc::clone(&task);
724 handles.push(scope.spawn(move || pollster::block_on(task(item))));
725 }
726
727 let mut chunk_results = Vec::with_capacity(handles.len());
728 for handle in handles {
729 let value = handle.join().map_err(|_| HttpError::WorkerPanicked {
730 operation: "ordered concurrent task",
731 })?;
732 chunk_results.push(value);
733 }
734 Ok::<Vec<T>, HttpError>(chunk_results)
735 })?;
736 results.extend(chunk_results);
737 }
738
739 Ok(results)
740}
741
742#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
745pub async fn map_ordered_concurrent<I, T, F, Fut>(
746 items: &[I],
747 concurrency: usize,
748 task: F,
749) -> Result<Vec<T>, HttpError>
750where
751 I: Clone,
752 F: Fn(I) -> Fut + Clone,
753 Fut: Future<Output = T>,
754{
755 let mut results = stream::iter(items.iter().cloned().enumerate().map(|(index, item)| {
756 let task = task.clone();
757 async move { (index, task(item).await) }
758 }))
759 .buffer_unordered(concurrency.max(1))
760 .collect::<Vec<_>>()
761 .await;
762
763 results.sort_by_key(|(index, _)| *index);
764 Ok(results.into_iter().map(|(_, value)| value).collect())
765}
766
767#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
770pub async fn map_ordered_concurrent<I, T, F, Fut>(
771 items: &[I],
772 _concurrency: usize,
773 task: F,
774) -> Result<Vec<T>, HttpError>
775where
776 I: Clone,
777 F: Fn(I) -> Fut,
778 Fut: Future<Output = T>,
779{
780 Ok(map_ordered_sequential(items, task).await)
781}
782
783#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
784async fn map_ordered_sequential<I, T, F, Fut>(items: &[I], task: F) -> Vec<T>
785where
786 I: Clone,
787 F: Fn(I) -> Fut,
788 Fut: Future<Output = T>,
789{
790 let mut results = Vec::with_capacity(items.len());
791 for item in items.iter().cloned() {
792 results.push(task(item).await);
793 }
794 results
795}
796
797pub type StubAnswer = dyn Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync;
799
800pub struct StubHttpClient {
814 answer: Box<StubAnswer>,
815}
816
817impl StubHttpClient {
818 pub fn new(
820 answer: impl Fn(&HttpRequest) -> Result<HttpResponse, HttpError> + Send + Sync + 'static,
821 ) -> Self {
822 Self {
823 answer: Box::new(answer),
824 }
825 }
826
827 pub fn with_body(body: impl Into<Vec<u8>>) -> Self {
829 let body = body.into();
830 Self::new(move |request| {
831 Ok(HttpResponse::new(
832 request.url.clone(),
833 200,
834 http_body_ref(BytesBody::new(body.clone())),
835 ))
836 })
837 }
838
839 pub fn from_text(
841 answer: impl Fn(&str) -> Result<String, HttpError> + Send + Sync + 'static,
842 ) -> Self {
843 Self::new(move |request| {
844 let body = answer(&request.url)?;
845 Ok(HttpResponse::new(
846 request.url.clone(),
847 200,
848 http_body_ref(BytesBody::new(body)),
849 ))
850 })
851 }
852}
853
854impl HttpClient for StubHttpClient {
855 fn send<'a>(
856 &'a self,
857 request: &'a HttpRequest,
858 _control: HttpControl,
859 ) -> HttpFuture<'a, HttpResponse> {
860 Box::pin(async move { (self.answer)(request) })
861 }
862}
863
864#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
865const CHUNK_LEN: usize = 64 * 1024;
866
867#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
868const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
869
870struct DefaultHttpClient {
871 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
872 native_client: Result<reqwest::blocking::Client, HttpError>,
873}
874
875impl DefaultHttpClient {
876 fn new() -> Self {
877 Self {
878 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
879 native_client: build_native_client(IDLE_TIMEOUT),
880 }
881 }
882
883 #[cfg(all(test, not(target_arch = "wasm32"), feature = "http-native"))]
884 fn with_idle_timeout(idle: std::time::Duration) -> Self {
885 Self {
886 native_client: build_native_client(idle),
887 }
888 }
889}
890
891#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
892struct ResponseHead {
893 status: u16,
894 headers: Vec<(String, String)>,
895 content_length: Option<u64>,
896 resumed: bool,
897}
898
899#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
900struct NativeTransfer {
901 head: crate::async_io::Signal<Result<ResponseHead, HttpError>>,
902 chunks: crate::async_io::ChunkStream<HttpError>,
903}
904
905#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
906impl CancelTransfer for NativeTransfer {
907 fn cancel(&self) {
908 self.head.set(Err(HttpError::Cancelled));
909 self.chunks.fail(HttpError::Cancelled);
910 }
911}
912
913#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
914struct ChannelBody {
915 transfer: Arc<NativeTransfer>,
916}
917
918#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
919impl HttpBody for ChannelBody {
920 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
921 Box::pin(self.transfer.chunks.next())
922 }
923}
924
925#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
926async fn send_native(
927 client: reqwest::blocking::Client,
928 request: HttpRequest,
929 control: HttpControl,
930) -> Result<HttpResponse, HttpError> {
931 use crate::async_io::{ChunkChannel, Signal};
932
933 let head = Signal::new();
934 let (chunks, stream) = ChunkChannel::<HttpError>::new();
935 let transfer = Arc::new(NativeTransfer {
936 head: head.clone(),
937 chunks: stream,
938 });
939 control.on_cancel(Arc::<NativeTransfer>::downgrade(&transfer));
940 let url = request.url.clone();
941
942 std::thread::Builder::new()
943 .name("cranpose-http".to_string())
944 .spawn(move || {
945 if let Err(error) = read_native_response(&client, request, &control, &chunks, &head) {
946 head.set(Err(error.clone()));
947 chunks.fail(error);
948 }
949 })
950 .map_err(|error| HttpError::RequestFailed {
951 url: url.clone(),
952 message: format!("could not start the transfer: {error}"),
953 })?;
954
955 let head = transfer
956 .head
957 .wait()
958 .await
959 .ok_or_else(|| HttpError::RequestFailed {
960 url: url.clone(),
961 message: "the transfer ended before a response arrived".to_string(),
962 })??;
963
964 Ok(
965 HttpResponse::new(url, head.status, http_body_ref(ChannelBody { transfer }))
966 .with_headers(head.headers)
967 .with_content_length(head.content_length)
968 .with_resumed(head.resumed),
969 )
970}
971
972#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
973fn read_native_response(
974 client: &reqwest::blocking::Client,
975 request: HttpRequest,
976 control: &HttpControl,
977 chunks: &crate::async_io::ChunkChannel<HttpError>,
978 head_signal: &crate::async_io::Signal<Result<ResponseHead, HttpError>>,
979) -> Result<(), HttpError> {
980 use std::io::Read;
981
982 if control.is_cancelled() {
983 return Err(HttpError::Cancelled);
984 }
985 let HttpRequest {
986 url,
987 method,
988 headers,
989 body,
990 resume_from,
991 } = request;
992 let method = match method {
993 HttpMethod::Get => reqwest::Method::GET,
994 HttpMethod::Head => reqwest::Method::HEAD,
995 HttpMethod::Post => reqwest::Method::POST,
996 HttpMethod::Put => reqwest::Method::PUT,
997 HttpMethod::Delete => reqwest::Method::DELETE,
998 };
999 let mut builder = client.request(method, &url);
1000 for (name, value) in headers {
1001 builder = builder.header(name, value);
1002 }
1003 if let Some(offset) = resume_from {
1004 builder = builder.header(reqwest::header::RANGE, format!("bytes={offset}-"));
1005 }
1006 if let Some(body) = body {
1007 builder = builder.body(body);
1008 }
1009
1010 let mut response = builder.send().map_err(|error| HttpError::RequestFailed {
1011 url: url.clone(),
1012 message: error.to_string(),
1013 })?;
1014
1015 let status = response.status();
1016 let resumed = resume_from.is_some() && status == reqwest::StatusCode::PARTIAL_CONTENT;
1017 let content_length = response.content_length();
1018 let headers = response
1019 .headers()
1020 .iter()
1021 .map(|(name, value)| {
1022 (
1023 name.as_str().to_ascii_lowercase(),
1024 value.to_str().unwrap_or_default().to_string(),
1025 )
1026 })
1027 .collect();
1028 head_signal.set(Ok(ResponseHead {
1029 status: status.as_u16(),
1030 headers,
1031 content_length,
1032 resumed,
1033 }));
1034
1035 let mut buffer = vec![0u8; CHUNK_LEN];
1036 let mut transferred = 0u64;
1037 loop {
1038 if control.is_cancelled() {
1039 return Err(HttpError::Cancelled);
1040 }
1041 let count = response
1042 .read(&mut buffer)
1043 .map_err(|error| HttpError::BodyReadFailed {
1044 url: url.clone(),
1045 message: error.to_string(),
1046 })?;
1047 if count == 0 {
1048 break;
1049 }
1050 transferred += count as u64;
1051 control.report(HttpProgress {
1052 transferred,
1053 total: content_length,
1054 });
1055 if !chunks.push(buffer[..count].to_vec()) {
1056 break;
1057 }
1058 }
1059 Ok(())
1060}
1061
1062#[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
1063async fn send_native(
1064 _request: HttpRequest,
1065 _control: HttpControl,
1066) -> Result<HttpResponse, HttpError> {
1067 Err(HttpError::UnsupportedFeature {
1068 operation: "native HTTP requests",
1069 feature: "http-native",
1070 })
1071}
1072
1073impl HttpClient for DefaultHttpClient {
1074 fn send<'a>(
1075 &'a self,
1076 request: &'a HttpRequest,
1077 control: HttpControl,
1078 ) -> HttpFuture<'a, HttpResponse> {
1079 Box::pin(async move {
1080 #[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1081 {
1082 let client = self.native_client.as_ref().map_err(Clone::clone)?.clone();
1083 send_native(client, request.clone(), control).await
1084 }
1085 #[cfg(all(not(target_arch = "wasm32"), not(feature = "http-native")))]
1086 {
1087 send_native(request.clone(), control).await
1088 }
1089 #[cfg(target_arch = "wasm32")]
1090 {
1091 send_web(request, control).await
1092 }
1093 })
1094 }
1095
1096 #[cfg(not(target_arch = "wasm32"))]
1097 fn download_to<'a>(
1098 &'a self,
1099 url: &'a str,
1100 target: &'a Path,
1101 control: HttpControl,
1102 ) -> HttpFuture<'a, u64> {
1103 Box::pin(async move {
1104 match PLATFORM_DOWNLOADER.get() {
1105 Some(downloader) => downloader.download(url, target, control).await,
1106 None => download_through(self, url, target, control).await,
1107 }
1108 })
1109 }
1110}
1111
1112#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1113fn build_native_client(idle: std::time::Duration) -> Result<reqwest::blocking::Client, HttpError> {
1114 configure_native_client_builder(
1115 reqwest::blocking::Client::builder()
1116 .connect_timeout(std::time::Duration::from_secs(30))
1117 .timeout(idle)
1118 .user_agent(concat!("cranpose/", env!("CARGO_PKG_VERSION"))),
1119 )?
1120 .build()
1121 .map_err(|err| HttpError::ClientInit(err.to_string()))
1122}
1123
1124#[cfg(all(not(target_arch = "wasm32"), feature = "http-native"))]
1125fn configure_native_client_builder(
1126 builder: reqwest::blocking::ClientBuilder,
1127) -> Result<reqwest::blocking::ClientBuilder, HttpError> {
1128 #[cfg(target_os = "android")]
1129 {
1130 Ok(builder.tls_certs_only(android_root_certificates()?))
1131 }
1132
1133 #[cfg(not(target_os = "android"))]
1134 {
1135 Ok(builder)
1136 }
1137}
1138
1139#[cfg(all(target_os = "android", feature = "http-native"))]
1140fn android_root_certificates() -> Result<Vec<reqwest::Certificate>, HttpError> {
1141 certificates_from_der_chain(
1142 webpki_root_certs::TLS_SERVER_ROOT_CERTS
1143 .iter()
1144 .map(AsRef::as_ref),
1145 )
1146}
1147
1148#[cfg(any(
1149 all(test, not(target_arch = "wasm32"), feature = "http-native"),
1150 all(target_os = "android", feature = "http-native")
1151))]
1152fn certificates_from_der_chain<'a, I>(
1153 certificates: I,
1154) -> Result<Vec<reqwest::Certificate>, HttpError>
1155where
1156 I: IntoIterator<Item = &'a [u8]>,
1157{
1158 certificates
1159 .into_iter()
1160 .enumerate()
1161 .map(|(index, der)| {
1162 reqwest::Certificate::from_der(der).map_err(|err| {
1163 HttpError::ClientInit(format!(
1164 "Failed to load TLS root certificate {index}: {err}"
1165 ))
1166 })
1167 })
1168 .collect()
1169}
1170
1171#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1172struct FetchBody {
1173 url: String,
1174 reader: web_sys::ReadableStreamDefaultReader,
1175 control: HttpControl,
1176 transferred: std::cell::Cell<u64>,
1177 total: Option<u64>,
1178}
1179
1180#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1181impl HttpBody for FetchBody {
1182 fn read_chunk(&self) -> HttpFuture<'_, Option<Vec<u8>>> {
1183 use wasm_bindgen_futures::JsFuture;
1184
1185 Box::pin(async move {
1186 if self.control.is_cancelled() {
1187 let _ = self.reader.cancel();
1188 return Err(HttpError::Cancelled);
1189 }
1190 let result = JsFuture::from(self.reader.read()).await.map_err(|error| {
1191 HttpError::BodyReadFailed {
1192 url: self.url.clone(),
1193 message: format!("{error:?}"),
1194 }
1195 })?;
1196 let done = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("done"))
1197 .ok()
1198 .and_then(|value| value.as_bool())
1199 .unwrap_or(true);
1200 if done {
1201 return Ok(None);
1202 }
1203 let value = js_sys::Reflect::get(&result, &wasm_bindgen::JsValue::from_str("value"))
1204 .map_err(|error| HttpError::BodyReadFailed {
1205 url: self.url.clone(),
1206 message: format!("{error:?}"),
1207 })?;
1208 let chunk = js_sys::Uint8Array::new(&value).to_vec();
1209 self.transferred
1210 .set(self.transferred.get() + chunk.len() as u64);
1211 self.control.report(HttpProgress {
1212 transferred: self.transferred.get(),
1213 total: self.total,
1214 });
1215 Ok(Some(chunk))
1216 })
1217 }
1218}
1219
1220#[cfg(all(target_arch = "wasm32", feature = "web-http"))]
1221async fn send_web(request: &HttpRequest, control: HttpControl) -> Result<HttpResponse, HttpError> {
1222 use wasm_bindgen::JsCast;
1223 use wasm_bindgen_futures::JsFuture;
1224 use web_sys::{Request, RequestInit, RequestMode, Response};
1225
1226 if control.is_cancelled() {
1227 return Err(HttpError::Cancelled);
1228 }
1229 let options = RequestInit::new();
1230 options.set_method(request.method.name());
1231 options.set_mode(RequestMode::Cors);
1232 if let Some(body) = &request.body {
1233 options.set_body(&js_sys::Uint8Array::from(body.as_slice()).into());
1234 }
1235
1236 let fetch_request =
1237 Request::new_with_str_and_init(&request.url, &options).map_err(|error| {
1238 HttpError::RequestFailed {
1239 url: request.url.clone(),
1240 message: format!("{error:?}"),
1241 }
1242 })?;
1243 let headers = fetch_request.headers();
1244 for (name, value) in &request.headers {
1245 headers
1246 .set(name, value)
1247 .map_err(|error| HttpError::RequestFailed {
1248 url: request.url.clone(),
1249 message: format!("{error:?}"),
1250 })?;
1251 }
1252 if let Some(offset) = request.resume_from {
1253 headers
1254 .set("Range", &format!("bytes={offset}-"))
1255 .map_err(|error| HttpError::RequestFailed {
1256 url: request.url.clone(),
1257 message: format!("{error:?}"),
1258 })?;
1259 }
1260
1261 let window = web_sys::window().ok_or(HttpError::NoWindow)?;
1262 let value = JsFuture::from(window.fetch_with_request(&fetch_request))
1263 .await
1264 .map_err(|error| HttpError::RequestFailed {
1265 url: request.url.clone(),
1266 message: format!("{error:?}"),
1267 })?;
1268 let response: Response = value.dyn_into().map_err(|_| HttpError::InvalidResponse {
1269 url: request.url.clone(),
1270 message: "the browser answered with something that is not a Response".to_string(),
1271 })?;
1272
1273 let status = response.status();
1274 let mut header_pairs = Vec::new();
1275 let entries = js_sys::try_iter(&response.headers()).ok().flatten();
1276 if let Some(entries) = entries {
1277 for entry in entries.flatten() {
1278 let pair = js_sys::Array::from(&entry);
1279 if pair.length() >= 2 {
1280 let name = pair.get(0).as_string().unwrap_or_default();
1281 let value = pair.get(1).as_string().unwrap_or_default();
1282 header_pairs.push((name.to_ascii_lowercase(), value));
1283 }
1284 }
1285 }
1286 let total = header_pairs
1287 .iter()
1288 .find(|(name, _)| name == "content-length")
1289 .and_then(|(_, value)| value.parse::<u64>().ok());
1290 let resumed = request.resume_from.is_some() && status == 206;
1291
1292 let body = response.body().ok_or_else(|| HttpError::InvalidResponse {
1293 url: request.url.clone(),
1294 message: "the response carries no body".to_string(),
1295 })?;
1296 let reader: web_sys::ReadableStreamDefaultReader =
1297 body.get_reader()
1298 .dyn_into()
1299 .map_err(|_| HttpError::InvalidResponse {
1300 url: request.url.clone(),
1301 message: "the response body cannot be read in chunks".to_string(),
1302 })?;
1303
1304 Ok(HttpResponse::new(
1305 request.url.clone(),
1306 status,
1307 http_body_ref(FetchBody {
1308 url: request.url.clone(),
1309 reader,
1310 control,
1311 transferred: std::cell::Cell::new(0),
1312 total,
1313 }),
1314 )
1315 .with_headers(header_pairs)
1316 .with_content_length(total)
1317 .with_resumed(resumed))
1318}
1319
1320#[cfg(all(target_arch = "wasm32", not(feature = "web-http")))]
1321async fn send_web(request: &HttpRequest, _control: HttpControl) -> Result<HttpResponse, HttpError> {
1322 let _ = request;
1323 Err(HttpError::UnsupportedFeature {
1324 operation: "web HTTP requests",
1325 feature: "web-http",
1326 })
1327}
1328
1329pub fn default_http_client() -> HttpClientRef {
1337 Arc::new(DefaultHttpClient::new())
1338}
1339
1340pub fn local_http_client() -> CompositionLocal<HttpClientRef> {
1343 crate::composition_locals::cached_local(
1344 |locals| &locals.http,
1345 || compositionLocalOfWithPolicy(default_http_client, Arc::ptr_eq),
1346 )
1347}
1348
1349#[cfg(test)]
1350#[path = "tests/http_tests.rs"]
1351mod tests;