deno_runtime/ops/web_worker/
sync_fetch.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use std::sync::Arc;
4
5use crate::web_worker::WebWorkerInternalHandle;
6use crate::web_worker::WebWorkerType;
7use deno_core::futures::StreamExt;
8use deno_core::op2;
9use deno_core::url::Url;
10use deno_core::OpState;
11use deno_fetch::data_url::DataUrl;
12use deno_fetch::FetchError;
13use deno_web::BlobStore;
14use http_body_util::BodyExt;
15use hyper::body::Bytes;
16use serde::Deserialize;
17use serde::Serialize;
18
19// TODO(andreubotella) Properly parse the MIME type
20fn mime_type_essence(mime_type: &str) -> String {
21  let essence = match mime_type.split_once(';') {
22    Some((essence, _)) => essence,
23    None => mime_type,
24  };
25  essence.trim().to_ascii_lowercase()
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum SyncFetchError {
30  #[error("Blob URLs are not supported in this context.")]
31  BlobUrlsNotSupportedInContext,
32  #[error("{0}")]
33  Io(#[from] std::io::Error),
34  #[error("Invalid script URL")]
35  InvalidScriptUrl,
36  #[error("http status error: {0}")]
37  InvalidStatusCode(http::StatusCode),
38  #[error("Classic scripts with scheme {0}: are not supported in workers")]
39  ClassicScriptSchemeUnsupportedInWorkers(String),
40  #[error("{0}")]
41  InvalidUri(#[from] http::uri::InvalidUri),
42  #[error("Invalid MIME type {0:?}.")]
43  InvalidMimeType(String),
44  #[error("Missing MIME type.")]
45  MissingMimeType,
46  #[error(transparent)]
47  Fetch(#[from] FetchError),
48  #[error(transparent)]
49  Join(#[from] tokio::task::JoinError),
50  #[error(transparent)]
51  Other(deno_core::error::AnyError),
52}
53
54#[derive(Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct SyncFetchScript {
57  url: String,
58  script: String,
59}
60
61#[op2]
62#[serde]
63pub fn op_worker_sync_fetch(
64  state: &mut OpState,
65  #[serde] scripts: Vec<String>,
66  loose_mime_checks: bool,
67) -> Result<Vec<SyncFetchScript>, SyncFetchError> {
68  let handle = state.borrow::<WebWorkerInternalHandle>().clone();
69  assert_eq!(handle.worker_type, WebWorkerType::Classic);
70
71  // it's not safe to share a client across tokio runtimes, so create a fresh one
72  // https://github.com/seanmonstar/reqwest/issues/1148#issuecomment-910868788
73  let options = state.borrow::<deno_fetch::Options>().clone();
74  let client = deno_fetch::create_client_from_options(&options)
75    .map_err(FetchError::ClientCreate)?;
76
77  // TODO(andreubotella) It's not good to throw an exception related to blob
78  // URLs when none of the script URLs use the blob scheme.
79  // Also, in which contexts are blob URLs not supported?
80  let blob_store = state
81    .try_borrow::<Arc<BlobStore>>()
82    .ok_or(SyncFetchError::BlobUrlsNotSupportedInContext)?
83    .clone();
84
85  // TODO(andreubotella): make the below thread into a resource that can be
86  // re-used. This would allow parallel fetching of multiple scripts.
87
88  let thread = std::thread::spawn(move || {
89    let runtime = tokio::runtime::Builder::new_current_thread()
90      .enable_io()
91      .enable_time()
92      .build()?;
93
94    runtime.block_on(async move {
95      let mut futures = scripts
96        .into_iter()
97        .map(|script| {
98          let client = client.clone();
99          let blob_store = blob_store.clone();
100          deno_core::unsync::spawn(async move {
101            let script_url = Url::parse(&script)
102              .map_err(|_| SyncFetchError::InvalidScriptUrl)?;
103            let mut loose_mime_checks = loose_mime_checks;
104
105            let (body, mime_type, res_url) = match script_url.scheme() {
106              "http" | "https" => {
107                let mut req = http::Request::new(
108                  http_body_util::Empty::new()
109                    .map_err(|never| match never {})
110                    .boxed(),
111                );
112                *req.uri_mut() = script_url.as_str().parse()?;
113
114                let resp =
115                  client.send(req).await.map_err(FetchError::ClientSend)?;
116
117                if resp.status().is_client_error()
118                  || resp.status().is_server_error()
119                {
120                  return Err(SyncFetchError::InvalidStatusCode(resp.status()));
121                }
122
123                // TODO(andreubotella) Properly run fetch's "extract a MIME type".
124                let mime_type = resp
125                  .headers()
126                  .get("Content-Type")
127                  .and_then(|v| v.to_str().ok())
128                  .map(mime_type_essence);
129
130                // Always check the MIME type with HTTP(S).
131                loose_mime_checks = false;
132
133                let body = resp
134                  .collect()
135                  .await
136                  .map_err(SyncFetchError::Other)?
137                  .to_bytes();
138
139                (body, mime_type, script)
140              }
141              "data" => {
142                let data_url =
143                  DataUrl::process(&script).map_err(FetchError::DataUrl)?;
144
145                let mime_type = {
146                  let mime = data_url.mime_type();
147                  format!("{}/{}", mime.type_, mime.subtype)
148                };
149
150                let (body, _) =
151                  data_url.decode_to_vec().map_err(FetchError::Base64)?;
152
153                (Bytes::from(body), Some(mime_type), script)
154              }
155              "blob" => {
156                let blob = blob_store
157                  .get_object_url(script_url)
158                  .ok_or(FetchError::BlobNotFound)?;
159
160                let mime_type = mime_type_essence(&blob.media_type);
161
162                let body = blob.read_all().await;
163
164                (Bytes::from(body), Some(mime_type), script)
165              }
166              _ => {
167                return Err(
168                  SyncFetchError::ClassicScriptSchemeUnsupportedInWorkers(
169                    script_url.scheme().to_string(),
170                  ),
171                )
172              }
173            };
174
175            if !loose_mime_checks {
176              // TODO(andreubotella) Check properly for a Javascript MIME type.
177              match mime_type.as_deref() {
178                Some("application/javascript" | "text/javascript") => {}
179                Some(mime_type) => {
180                  return Err(SyncFetchError::InvalidMimeType(
181                    mime_type.to_string(),
182                  ))
183                }
184                None => return Err(SyncFetchError::MissingMimeType),
185              }
186            }
187
188            let (text, _) = encoding_rs::UTF_8.decode_with_bom_removal(&body);
189
190            Ok(SyncFetchScript {
191              url: res_url,
192              script: text.into_owned(),
193            })
194          })
195        })
196        .collect::<deno_core::futures::stream::FuturesUnordered<_>>();
197      let mut ret = Vec::with_capacity(futures.len());
198      while let Some(result) = futures.next().await {
199        let script = result??;
200        ret.push(script);
201      }
202      Ok(ret)
203    })
204  });
205
206  thread.join().unwrap()
207}