1use anyhow::anyhow;
2use futures::AsyncReadExt as _;
3use http_client::{AsyncBody, HttpClient, RedirectPolicy};
4use std::future::Future;
5use std::pin::Pin;
6use std::task::Poll;
7use wasm_bindgen::JsCast as _;
8use wasm_bindgen::prelude::*;
9
10#[wasm_bindgen]
11extern "C" {
12 #[wasm_bindgen(catch, js_name = "fetch")]
13 fn global_fetch(input: &web_sys::Request) -> Result<js_sys::Promise, JsValue>;
14}
15
16pub struct FetchHttpClient {
17 user_agent: Option<http_client::http::header::HeaderValue>,
18}
19
20impl Default for FetchHttpClient {
21 fn default() -> Self {
22 Self { user_agent: None }
23 }
24}
25
26#[cfg(feature = "multithreaded")]
27impl FetchHttpClient {
28 /// # Safety
29 ///
30 /// The caller must ensure that the created `FetchHttpClient` is only used in a single thread environment.
31 pub unsafe fn new() -> Self {
32 Self::default()
33 }
34
35 /// # Safety
36 ///
37 /// The caller must ensure that the created `FetchHttpClient` is only used in a single thread environment.
38 pub unsafe fn with_user_agent(user_agent: &str) -> anyhow::Result<Self> {
39 Ok(Self {
40 user_agent: Some(http_client::http::header::HeaderValue::from_str(
41 user_agent,
42 )?),
43 })
44 }
45}
46
47#[cfg(not(feature = "multithreaded"))]
48impl FetchHttpClient {
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn with_user_agent(user_agent: &str) -> anyhow::Result<Self> {
54 Ok(Self {
55 user_agent: Some(http_client::http::header::HeaderValue::from_str(
56 user_agent,
57 )?),
58 })
59 }
60}
61
62/// Wraps a `!Send` future to satisfy the `Send` bound on `BoxFuture`.
63///
64/// Safety: only valid in WASM contexts where the `FetchHttpClient` is
65/// confined to a single thread (guaranteed by the caller via unsafe
66/// constructors when `multithreaded` is enabled, or by the absence of
67/// threads when it is not).
68struct AssertSend<F>(F);
69
70unsafe impl<F> Send for AssertSend<F> {}
71
72impl<F: Future> Future for AssertSend<F> {
73 type Output = F::Output;
74
75 fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
76 // Safety: pin projection for a single-field newtype wrapper.
77 let inner = unsafe { self.map_unchecked_mut(|this| &mut this.0) };
78 inner.poll(cx)
79 }
80}
81
82impl HttpClient for FetchHttpClient {
83 fn user_agent(&self) -> Option<&http_client::http::header::HeaderValue> {
84 self.user_agent.as_ref()
85 }
86
87 fn proxy(&self) -> Option<&http_client::Url> {
88 None
89 }
90
91 fn send(
92 &self,
93 req: http_client::http::Request<AsyncBody>,
94 ) -> futures::future::BoxFuture<'static, anyhow::Result<http_client::http::Response<AsyncBody>>>
95 {
96 let (parts, body) = req.into_parts();
97
98 Box::pin(AssertSend(async move {
99 let body_bytes = read_body_to_bytes(body).await?;
100
101 let init = web_sys::RequestInit::new();
102 init.set_method(parts.method.as_str());
103
104 if let Some(redirect_policy) = parts.extensions.get::<RedirectPolicy>() {
105 match redirect_policy {
106 RedirectPolicy::NoFollow => {
107 init.set_redirect(web_sys::RequestRedirect::Manual);
108 }
109 RedirectPolicy::FollowLimit(_) | RedirectPolicy::FollowAll => {
110 init.set_redirect(web_sys::RequestRedirect::Follow);
111 }
112 }
113 }
114
115 if let Some(ref bytes) = body_bytes {
116 let uint8array = js_sys::Uint8Array::from(bytes.as_slice());
117 init.set_body(uint8array.as_ref());
118 }
119
120 let url = parts.uri.to_string();
121 let request = web_sys::Request::new_with_str_and_init(&url, &init)
122 .map_err(|error| anyhow!("failed to create fetch Request: {error:?}"))?;
123
124 let request_headers = request.headers();
125 for (name, value) in &parts.headers {
126 let value_str = value
127 .to_str()
128 .map_err(|_| anyhow!("non-ASCII header value for {name}"))?;
129 request_headers
130 .set(name.as_str(), value_str)
131 .map_err(|error| anyhow!("failed to set header {name}: {error:?}"))?;
132 }
133
134 let promise = global_fetch(&request)
135 .map_err(|error| anyhow!("fetch threw an error: {error:?}"))?;
136 let response_value = wasm_bindgen_futures::JsFuture::from(promise)
137 .await
138 .map_err(|error| anyhow!("fetch failed: {error:?}"))?;
139
140 let web_response: web_sys::Response = response_value
141 .dyn_into()
142 .map_err(|error| anyhow!("fetch result is not a Response: {error:?}"))?;
143
144 let status = web_response.status();
145 let mut builder = http_client::http::Response::builder().status(status);
146
147 // `Headers` is a JS iterable yielding `[name, value]` pairs.
148 // `js_sys::Array::from` calls `Array.from()` which accepts any iterable.
149 let header_pairs = js_sys::Array::from(&web_response.headers());
150 for index in 0..header_pairs.length() {
151 match header_pairs.get(index).dyn_into::<js_sys::Array>() {
152 Ok(pair) => match (pair.get(0).as_string(), pair.get(1).as_string()) {
153 (Some(name), Some(value)) => {
154 builder = builder.header(name, value);
155 }
156 (name, value) => {
157 log::warn!(
158 "skipping response header at index {index}: \
159 name={name:?}, value={value:?}"
160 );
161 }
162 },
163 Err(entry) => {
164 log::warn!("skipping non-array header entry at index {index}: {entry:?}");
165 }
166 }
167 }
168
169 // The entire response body is eagerly buffered into memory via
170 // `arrayBuffer()`. The Fetch API does not expose a synchronous
171 // streaming interface; streaming would require `ReadableStream`
172 // interop which is significantly more complex.
173 let body_promise = web_response
174 .array_buffer()
175 .map_err(|error| anyhow!("failed to initiate response body read: {error:?}"))?;
176 let body_value = wasm_bindgen_futures::JsFuture::from(body_promise)
177 .await
178 .map_err(|error| anyhow!("failed to read response body: {error:?}"))?;
179 let array_buffer: js_sys::ArrayBuffer = body_value
180 .dyn_into()
181 .map_err(|error| anyhow!("response body is not an ArrayBuffer: {error:?}"))?;
182 let response_bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
183
184 builder
185 .body(AsyncBody::from(response_bytes))
186 .map_err(|error| anyhow!(error))
187 }))
188 }
189}
190
191async fn read_body_to_bytes(mut body: AsyncBody) -> anyhow::Result<Option<Vec<u8>>> {
192 let mut buffer = Vec::new();
193 body.read_to_end(&mut buffer).await?;
194 if buffer.is_empty() {
195 Ok(None)
196 } else {
197 Ok(Some(buffer))
198 }
199}