// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::cache::HttpCache; use crate::cache::RealDenoCacheEnv; use crate::colors; use crate::http_util::get_response_body_with_progress; use crate::http_util::HttpClientProvider; use crate::util::progress_bar::ProgressBar; use boxed_error::Boxed; use deno_ast::MediaType; use deno_cache_dir::file_fetcher::AuthTokens; use deno_cache_dir::file_fetcher::BlobData; use deno_cache_dir::file_fetcher::CacheSetting; use deno_cache_dir::file_fetcher::FetchNoFollowError; use deno_cache_dir::file_fetcher::File; use deno_cache_dir::file_fetcher::FileFetcherOptions; use deno_cache_dir::file_fetcher::FileOrRedirect; use deno_cache_dir::file_fetcher::SendError; use deno_cache_dir::file_fetcher::SendResponse; use deno_cache_dir::file_fetcher::TooManyRedirectsError; use deno_cache_dir::file_fetcher::UnsupportedSchemeError; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_error::JsError; use deno_graph::source::LoaderChecksum; use deno_runtime::deno_permissions::CheckSpecifierKind; use deno_runtime::deno_permissions::PermissionCheckError; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_web::BlobStore; use http::header; use http::HeaderMap; use http::StatusCode; use std::borrow::Cow; use std::collections::HashMap; use std::env; use std::sync::Arc; use thiserror::Error; #[derive(Debug, Clone, Eq, PartialEq)] pub struct TextDecodedFile { pub media_type: MediaType, /// The _final_ specifier for the file. The requested specifier and the final /// specifier maybe different for remote files that have been redirected. pub specifier: ModuleSpecifier, /// The source of the file. pub source: Arc, } impl TextDecodedFile { /// Decodes the source bytes into a string handling any encoding rules /// for local vs remote files and dealing with the charset. pub fn decode(file: File) -> Result { let (media_type, maybe_charset) = deno_graph::source::resolve_media_type_and_charset_from_headers( &file.url, file.maybe_headers.as_ref(), ); let specifier = file.url; match deno_graph::source::decode_source( &specifier, file.source, maybe_charset, ) { Ok(source) => Ok(TextDecodedFile { media_type, specifier, source, }), Err(err) => { Err(err).with_context(|| format!("Failed decoding \"{}\".", specifier)) } } } } #[derive(Debug)] struct BlobStoreAdapter(Arc); #[async_trait::async_trait(?Send)] impl deno_cache_dir::file_fetcher::BlobStore for BlobStoreAdapter { async fn get(&self, specifier: &Url) -> std::io::Result> { let Some(blob) = self.0.get_object_url(specifier.clone()) else { return Ok(None); }; Ok(Some(BlobData { media_type: blob.media_type.clone(), bytes: blob.read_all().await, })) } } #[derive(Debug)] struct HttpClientAdapter { http_client_provider: Arc, download_log_level: log::Level, progress_bar: Option, } #[async_trait::async_trait(?Send)] impl deno_cache_dir::file_fetcher::HttpClient for HttpClientAdapter { async fn send_no_follow( &self, url: &Url, headers: HeaderMap, ) -> Result { async fn handle_request_or_server_error( retried: &mut bool, specifier: &Url, err_str: String, ) -> Result<(), ()> { // Retry once, and bail otherwise. if !*retried { *retried = true; log::debug!("Import '{}' failed: {}. Retrying...", specifier, err_str); tokio::time::sleep(std::time::Duration::from_millis(50)).await; Ok(()) } else { Err(()) } } let mut maybe_progress_guard = None; if let Some(pb) = self.progress_bar.as_ref() { maybe_progress_guard = Some(pb.update(url.as_str())); } else { log::log!( self.download_log_level, "{} {}", colors::green("Download"), url ); } let mut retried = false; // retry intermittent failures loop { let response = match self .http_client_provider .get_or_create() .map_err(|err| SendError::Failed(err.into()))? .send(url, headers.clone()) .await { Ok(response) => response, Err(crate::http_util::SendError::Send(err)) => { if err.is_connect_error() { handle_request_or_server_error(&mut retried, url, err.to_string()) .await .map_err(|()| SendError::Failed(err.into()))?; continue; } else { return Err(SendError::Failed(err.into())); } } Err(crate::http_util::SendError::InvalidUri(err)) => { return Err(SendError::Failed(err.into())); } }; if response.status() == StatusCode::NOT_MODIFIED { return Ok(SendResponse::NotModified); } if let Some(warning) = response.headers().get("X-Deno-Warning") { log::warn!( "{} {}", crate::colors::yellow("Warning"), warning.to_str().unwrap() ); } if response.status().is_redirection() { return Ok(SendResponse::Redirect(response.into_parts().0.headers)); } if response.status().is_server_error() { handle_request_or_server_error( &mut retried, url, response.status().to_string(), ) .await .map_err(|()| SendError::StatusCode(response.status()))?; } else if response.status().is_client_error() { let err = if response.status() == StatusCode::NOT_FOUND { SendError::NotFound } else { SendError::StatusCode(response.status()) }; return Err(err); } else { let body_result = get_response_body_with_progress( response, maybe_progress_guard.as_ref(), ) .await; match body_result { Ok((headers, body)) => { return Ok(SendResponse::Success(headers, body)); } Err(err) => { handle_request_or_server_error(&mut retried, url, err.to_string()) .await .map_err(|()| SendError::Failed(err.into()))?; continue; } } } } } } #[derive(Debug, Default)] struct MemoryFiles(Mutex>); impl MemoryFiles { pub fn insert(&self, specifier: ModuleSpecifier, file: File) -> Option { self.0.lock().insert(specifier, file) } pub fn clear(&self) { self.0.lock().clear(); } } impl deno_cache_dir::file_fetcher::MemoryFiles for MemoryFiles { fn get(&self, specifier: &ModuleSpecifier) -> Option { self.0.lock().get(specifier).cloned() } } #[derive(Debug, Boxed, JsError)] pub struct CliFetchNoFollowError(pub Box); #[derive(Debug, Error, JsError)] pub enum CliFetchNoFollowErrorKind { #[error(transparent)] #[class(inherit)] FetchNoFollow(#[from] FetchNoFollowError), #[error(transparent)] #[class(generic)] PermissionCheck(#[from] PermissionCheckError), } #[derive(Debug, Copy, Clone)] pub enum FetchPermissionsOptionRef<'a> { AllowAll, Restricted(&'a PermissionsContainer, CheckSpecifierKind), } #[derive(Debug, Default)] pub struct FetchOptions<'a> { pub maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, pub maybe_accept: Option<&'a str>, pub maybe_cache_setting: Option<&'a CacheSetting>, } pub struct FetchNoFollowOptions<'a> { pub maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, pub maybe_accept: Option<&'a str>, pub maybe_cache_setting: Option<&'a CacheSetting>, pub maybe_checksum: Option<&'a LoaderChecksum>, } type DenoCacheDirFileFetcher = deno_cache_dir::file_fetcher::FileFetcher< BlobStoreAdapter, RealDenoCacheEnv, HttpClientAdapter, >; /// A structure for resolving, fetching and caching source files. #[derive(Debug)] pub struct CliFileFetcher { file_fetcher: DenoCacheDirFileFetcher, memory_files: Arc, } impl CliFileFetcher { pub fn new( http_cache: Arc, http_client_provider: Arc, blob_store: Arc, progress_bar: Option, allow_remote: bool, cache_setting: CacheSetting, download_log_level: log::Level, ) -> Self { let memory_files = Arc::new(MemoryFiles::default()); let file_fetcher = DenoCacheDirFileFetcher::new( BlobStoreAdapter(blob_store), RealDenoCacheEnv, http_cache, HttpClientAdapter { http_client_provider: http_client_provider.clone(), download_log_level, progress_bar, }, memory_files.clone(), FileFetcherOptions { allow_remote, cache_setting, auth_tokens: AuthTokens::new(env::var("DENO_AUTH_TOKENS").ok()), }, ); Self { file_fetcher, memory_files, } } pub fn cache_setting(&self) -> &CacheSetting { self.file_fetcher.cache_setting() } #[inline(always)] pub async fn fetch_bypass_permissions( &self, specifier: &ModuleSpecifier, ) -> Result { self .fetch_inner(specifier, None, FetchPermissionsOptionRef::AllowAll) .await } #[inline(always)] pub async fn fetch_bypass_permissions_with_maybe_auth( &self, specifier: &ModuleSpecifier, maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, ) -> Result { self .fetch_inner(specifier, maybe_auth, FetchPermissionsOptionRef::AllowAll) .await } /// Fetch a source file and asynchronously return it. #[inline(always)] pub async fn fetch( &self, specifier: &ModuleSpecifier, permissions: &PermissionsContainer, ) -> Result { self .fetch_inner( specifier, None, FetchPermissionsOptionRef::Restricted( permissions, CheckSpecifierKind::Static, ), ) .await } async fn fetch_inner( &self, specifier: &ModuleSpecifier, maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, permissions: FetchPermissionsOptionRef<'_>, ) -> Result { self .fetch_with_options( specifier, permissions, FetchOptions { maybe_auth, maybe_accept: None, maybe_cache_setting: None, }, ) .await } pub async fn fetch_with_options( &self, specifier: &ModuleSpecifier, permissions: FetchPermissionsOptionRef<'_>, options: FetchOptions<'_>, ) -> Result { self .fetch_with_options_and_max_redirect(specifier, permissions, options, 10) .await } async fn fetch_with_options_and_max_redirect( &self, specifier: &ModuleSpecifier, permissions: FetchPermissionsOptionRef<'_>, options: FetchOptions<'_>, max_redirect: usize, ) -> Result { let mut specifier = Cow::Borrowed(specifier); let mut maybe_auth = options.maybe_auth; for _ in 0..=max_redirect { match self .fetch_no_follow( &specifier, permissions, FetchNoFollowOptions { maybe_auth: maybe_auth.clone(), maybe_accept: options.maybe_accept, maybe_cache_setting: options.maybe_cache_setting, maybe_checksum: None, }, ) .await? { FileOrRedirect::File(file) => { return Ok(file); } FileOrRedirect::Redirect(redirect_specifier) => { // If we were redirected to another origin, don't send the auth header anymore. if redirect_specifier.origin() != specifier.origin() { maybe_auth = None; } specifier = Cow::Owned(redirect_specifier); } } } Err(TooManyRedirectsError(specifier.into_owned()).into()) } /// Fetches without following redirects. pub async fn fetch_no_follow( &self, specifier: &ModuleSpecifier, permissions: FetchPermissionsOptionRef<'_>, options: FetchNoFollowOptions<'_>, ) -> Result { validate_scheme(specifier).map_err(|err| { CliFetchNoFollowErrorKind::FetchNoFollow(err.into()).into_box() })?; match permissions { FetchPermissionsOptionRef::AllowAll => { // allow } FetchPermissionsOptionRef::Restricted(permissions, kind) => { permissions.check_specifier(specifier, kind)?; } } self .file_fetcher .fetch_no_follow( specifier, deno_cache_dir::file_fetcher::FetchNoFollowOptions { maybe_auth: options.maybe_auth, maybe_checksum: options .maybe_checksum .map(|c| deno_cache_dir::Checksum::new(c.as_str())), maybe_accept: options.maybe_accept, maybe_cache_setting: options.maybe_cache_setting, }, ) .await .map_err(|err| CliFetchNoFollowErrorKind::FetchNoFollow(err).into_box()) } /// A synchronous way to retrieve a source file, where if the file has already /// been cached in memory it will be returned, otherwise for local files will /// be read from disk. pub fn get_cached_source_or_local( &self, specifier: &ModuleSpecifier, ) -> Result, AnyError> { if specifier.scheme() == "file" { Ok(self.file_fetcher.fetch_local(specifier)?) } else { Ok(self.file_fetcher.fetch_cached(specifier, 10)?) } } /// Insert a temporary module for the file fetcher. pub fn insert_memory_files(&self, file: File) -> Option { self.memory_files.insert(file.url.clone(), file) } pub fn clear_memory_files(&self) { self.memory_files.clear(); } } fn validate_scheme(specifier: &Url) -> Result<(), UnsupportedSchemeError> { match deno_cache_dir::file_fetcher::is_valid_scheme(specifier.scheme()) { true => Ok(()), false => Err(UnsupportedSchemeError { scheme: specifier.scheme().to_string(), url: specifier.clone(), }), } } #[cfg(test)] mod tests { use crate::cache::GlobalHttpCache; use crate::cache::RealDenoCacheEnv; use crate::http_util::HttpClientProvider; use super::*; use deno_cache_dir::file_fetcher::FetchNoFollowErrorKind; use deno_cache_dir::file_fetcher::HttpClient; use deno_core::resolve_url; use deno_runtime::deno_web::Blob; use deno_runtime::deno_web::InMemoryBlobPart; use test_util::TempDir; fn setup( cache_setting: CacheSetting, maybe_temp_dir: Option, ) -> (CliFileFetcher, TempDir) { let (file_fetcher, temp_dir, _) = setup_with_blob_store(cache_setting, maybe_temp_dir); (file_fetcher, temp_dir) } fn setup_with_blob_store( cache_setting: CacheSetting, maybe_temp_dir: Option, ) -> (CliFileFetcher, TempDir, Arc) { let (file_fetcher, temp_dir, blob_store, _) = setup_with_blob_store_and_cache(cache_setting, maybe_temp_dir); (file_fetcher, temp_dir, blob_store) } fn setup_with_blob_store_and_cache( cache_setting: CacheSetting, maybe_temp_dir: Option, ) -> ( CliFileFetcher, TempDir, Arc, Arc, ) { let temp_dir = maybe_temp_dir.unwrap_or_default(); let location = temp_dir.path().join("remote").to_path_buf(); let blob_store: Arc = Default::default(); let cache = Arc::new(GlobalHttpCache::new(location, RealDenoCacheEnv)); let file_fetcher = CliFileFetcher::new( cache.clone(), Arc::new(HttpClientProvider::new(None, None)), blob_store.clone(), None, true, cache_setting, log::Level::Info, ); (file_fetcher, temp_dir, blob_store, cache) } async fn test_fetch(specifier: &ModuleSpecifier) -> (File, CliFileFetcher) { let (file_fetcher, _) = setup(CacheSetting::ReloadAll, None); let result = file_fetcher.fetch_bypass_permissions(specifier).await; assert!(result.is_ok()); (result.unwrap(), file_fetcher) } async fn test_fetch_options_remote( specifier: &ModuleSpecifier, ) -> (File, HashMap) { let _http_server_guard = test_util::http_server(); let (file_fetcher, _, _, http_cache) = setup_with_blob_store_and_cache(CacheSetting::ReloadAll, None); let result: Result = file_fetcher .fetch_with_options_and_max_redirect( specifier, FetchPermissionsOptionRef::AllowAll, Default::default(), 1, ) .await; let cache_key = http_cache.cache_item_key(specifier).unwrap(); ( result.unwrap(), http_cache.read_headers(&cache_key).unwrap().unwrap(), ) } // this test used to test how the file fetcher decoded strings, but // now we're using it as a bit of an integration test with the functionality // in deno_graph async fn test_fetch_remote_encoded( fixture: &str, charset: &str, expected: &str, ) { let url_str = format!("http://127.0.0.1:4545/encoding/{fixture}"); let specifier = resolve_url(&url_str).unwrap(); let (file, headers) = test_fetch_options_remote(&specifier).await; let (media_type, maybe_charset) = deno_graph::source::resolve_media_type_and_charset_from_headers( &specifier, Some(&headers), ); assert_eq!( deno_graph::source::decode_source(&specifier, file.source, maybe_charset) .unwrap() .as_ref(), expected ); assert_eq!(media_type, MediaType::TypeScript); assert_eq!( headers.get("content-type").unwrap(), &format!("application/typescript;charset={charset}") ); } async fn test_fetch_local_encoded(charset: &str, expected: String) { let p = test_util::testdata_path().join(format!("encoding/{charset}.ts")); let specifier = ModuleSpecifier::from_file_path(p).unwrap(); let (file, _) = test_fetch(&specifier).await; assert_eq!( deno_graph::source::decode_source(&specifier, file.source, None) .unwrap() .as_ref(), expected ); } #[tokio::test] async fn test_insert_cached() { let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None); let local = temp_dir.path().join("a.ts"); let specifier = ModuleSpecifier::from_file_path(&local).unwrap(); let file = File { source: Arc::from("some source code".as_bytes()), url: specifier.clone(), maybe_headers: Some(HashMap::from([( "content-type".to_string(), "application/javascript".to_string(), )])), }; file_fetcher.insert_memory_files(file.clone()); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let result_file = result.unwrap(); assert_eq!(result_file, file); } #[tokio::test] async fn test_fetch_data_url() { let (file_fetcher, _) = setup(CacheSetting::Use, None); let specifier = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!( &*file.source, "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n" ); assert_eq!(file.media_type, MediaType::TypeScript); assert_eq!(file.specifier, specifier); } #[tokio::test] async fn test_fetch_blob_url() { let (file_fetcher, _, blob_store) = setup_with_blob_store(CacheSetting::Use, None); let bytes = "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n" .as_bytes() .to_vec(); let specifier = blob_store.insert_object_url( Blob { media_type: "application/typescript".to_string(), parts: vec![Arc::new(InMemoryBlobPart::from(bytes))], }, None, ); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!( &*file.source, "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n" ); assert_eq!(file.media_type, MediaType::TypeScript); assert_eq!(file.specifier, specifier); } #[tokio::test] async fn test_fetch_complex() { let _http_server_guard = test_util::http_server(); let (file_fetcher, temp_dir, _, http_cache) = setup_with_blob_store_and_cache(CacheSetting::Use, None); let (file_fetcher_01, _) = setup(CacheSetting::Use, Some(temp_dir.clone())); let (file_fetcher_02, _, _, http_cache_02) = setup_with_blob_store_and_cache( CacheSetting::Use, Some(temp_dir.clone()), ); let specifier = ModuleSpecifier::parse("http://localhost:4545/subdir/mod2.ts").unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!( &*file.source, "export { printHello } from \"./print_hello.ts\";\n" ); assert_eq!(file.media_type, MediaType::TypeScript); let cache_item_key = http_cache.cache_item_key(&specifier).unwrap(); let mut headers = HashMap::new(); headers.insert("content-type".to_string(), "text/javascript".to_string()); http_cache .set(&specifier, headers.clone(), file.source.as_bytes()) .unwrap(); let result = file_fetcher_01.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!( &*file.source, "export { printHello } from \"./print_hello.ts\";\n" ); // This validates that when using the cached value, because we modified // the value above. assert_eq!(file.media_type, MediaType::JavaScript); let headers2 = http_cache_02 .read_headers(&cache_item_key) .unwrap() .unwrap(); assert_eq!(headers2.get("content-type").unwrap(), "text/javascript"); headers = HashMap::new(); headers.insert("content-type".to_string(), "application/json".to_string()); http_cache_02 .set(&specifier, headers.clone(), file.source.as_bytes()) .unwrap(); let result = file_fetcher_02.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!( &*file.source, "export { printHello } from \"./print_hello.ts\";\n" ); assert_eq!(file.media_type, MediaType::Json); // This creates a totally new instance, simulating another Deno process // invocation and indicates to "cache bust". let location = temp_dir.path().join("remote").to_path_buf(); let file_fetcher = CliFileFetcher::new( Arc::new(GlobalHttpCache::new( location, crate::cache::RealDenoCacheEnv, )), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, true, CacheSetting::ReloadAll, log::Level::Info, ); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!( &*file.source, "export { printHello } from \"./print_hello.ts\";\n" ); assert_eq!(file.media_type, MediaType::TypeScript); } #[tokio::test] async fn test_fetch_uses_cache() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); let location = temp_dir.path().join("remote").to_path_buf(); let specifier = resolve_url("http://localhost:4545/subdir/mismatch_ext.ts").unwrap(); let http_cache = Arc::new(GlobalHttpCache::new( location.clone(), crate::cache::RealDenoCacheEnv, )); let file_modified_01 = { let file_fetcher = CliFileFetcher::new( http_cache.clone(), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, true, CacheSetting::Use, log::Level::Info, ); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let cache_key = http_cache.cache_item_key(&specifier).unwrap(); ( http_cache.read_modified_time(&cache_key).unwrap(), http_cache.read_headers(&cache_key).unwrap().unwrap(), http_cache.read_download_time(&cache_key).unwrap().unwrap(), ) }; let file_modified_02 = { let file_fetcher = CliFileFetcher::new( Arc::new(GlobalHttpCache::new( location, crate::cache::RealDenoCacheEnv, )), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, true, CacheSetting::Use, log::Level::Info, ); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let cache_key = http_cache.cache_item_key(&specifier).unwrap(); ( http_cache.read_modified_time(&cache_key).unwrap(), http_cache.read_headers(&cache_key).unwrap().unwrap(), http_cache.read_download_time(&cache_key).unwrap().unwrap(), ) }; assert_eq!(file_modified_01, file_modified_02); } #[tokio::test] async fn test_fetch_redirected() { let _http_server_guard = test_util::http_server(); let (file_fetcher, _, _, http_cache) = setup_with_blob_store_and_cache(CacheSetting::Use, None); let specifier = resolve_url("http://localhost:4546/subdir/redirects/redirect1.js") .unwrap(); let redirected_specifier = resolve_url("http://localhost:4545/subdir/redirects/redirect1.js") .unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); assert_eq!(file.url, redirected_specifier); assert_eq!( get_text_from_cache(http_cache.as_ref(), &specifier), "", "redirected files should have empty cached contents" ); assert_eq!( get_location_header_from_cache(http_cache.as_ref(), &specifier), Some("http://localhost:4545/subdir/redirects/redirect1.js".to_string()), ); assert_eq!( get_text_from_cache(http_cache.as_ref(), &redirected_specifier), "export const redirect = 1;\n" ); assert_eq!( get_location_header_from_cache( http_cache.as_ref(), &redirected_specifier ), None, ); } #[tokio::test] async fn test_fetch_multiple_redirects() { let _http_server_guard = test_util::http_server(); let (file_fetcher, _, _, http_cache) = setup_with_blob_store_and_cache(CacheSetting::Use, None); let specifier = resolve_url("http://localhost:4548/subdir/redirects/redirect1.js") .unwrap(); let redirected_01_specifier = resolve_url("http://localhost:4546/subdir/redirects/redirect1.js") .unwrap(); let redirected_02_specifier = resolve_url("http://localhost:4545/subdir/redirects/redirect1.js") .unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); assert_eq!(file.url, redirected_02_specifier); assert_eq!( get_text_from_cache(http_cache.as_ref(), &specifier), "", "redirected files should have empty cached contents" ); assert_eq!( get_location_header_from_cache(http_cache.as_ref(), &specifier), Some("http://localhost:4546/subdir/redirects/redirect1.js".to_string()), ); assert_eq!( get_text_from_cache(http_cache.as_ref(), &redirected_01_specifier), "", "redirected files should have empty cached contents" ); assert_eq!( get_location_header_from_cache( http_cache.as_ref(), &redirected_01_specifier ), Some("http://localhost:4545/subdir/redirects/redirect1.js".to_string()), ); assert_eq!( get_text_from_cache(http_cache.as_ref(), &redirected_02_specifier), "export const redirect = 1;\n" ); assert_eq!( get_location_header_from_cache( http_cache.as_ref(), &redirected_02_specifier ), None, ); } #[tokio::test] async fn test_fetch_uses_cache_with_redirects() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); let location = temp_dir.path().join("remote").to_path_buf(); let specifier = resolve_url("http://localhost:4548/subdir/mismatch_ext.ts").unwrap(); let redirected_specifier = resolve_url("http://localhost:4546/subdir/mismatch_ext.ts").unwrap(); let http_cache = Arc::new(GlobalHttpCache::new( location.clone(), crate::cache::RealDenoCacheEnv, )); let metadata_file_modified_01 = { let file_fetcher = CliFileFetcher::new( http_cache.clone(), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, true, CacheSetting::Use, log::Level::Info, ); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let cache_key = http_cache.cache_item_key(&redirected_specifier).unwrap(); ( http_cache.read_modified_time(&cache_key).unwrap(), http_cache.read_headers(&cache_key).unwrap().unwrap(), http_cache.read_download_time(&cache_key).unwrap().unwrap(), ) }; let metadata_file_modified_02 = { let file_fetcher = CliFileFetcher::new( http_cache.clone(), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, true, CacheSetting::Use, log::Level::Info, ); let result = file_fetcher .fetch_bypass_permissions(&redirected_specifier) .await; assert!(result.is_ok()); let cache_key = http_cache.cache_item_key(&redirected_specifier).unwrap(); ( http_cache.read_modified_time(&cache_key).unwrap(), http_cache.read_headers(&cache_key).unwrap().unwrap(), http_cache.read_download_time(&cache_key).unwrap().unwrap(), ) }; assert_eq!(metadata_file_modified_01, metadata_file_modified_02); } #[tokio::test] async fn test_fetcher_limits_redirects() { let _http_server_guard = test_util::http_server(); let (file_fetcher, _) = setup(CacheSetting::Use, None); let specifier = resolve_url("http://localhost:4548/subdir/redirects/redirect1.js") .unwrap(); let result = file_fetcher .fetch_with_options_and_max_redirect( &specifier, FetchPermissionsOptionRef::AllowAll, Default::default(), 2, ) .await; assert!(result.is_ok()); let result = file_fetcher .fetch_with_options_and_max_redirect( &specifier, FetchPermissionsOptionRef::AllowAll, Default::default(), 1, ) .await; assert!(result.is_err()); let result = file_fetcher.file_fetcher.fetch_cached(&specifier, 2); assert!(result.is_ok()); let result = file_fetcher.file_fetcher.fetch_cached(&specifier, 1); assert!(result.is_err()); } #[tokio::test] async fn test_fetch_same_host_redirect() { let _http_server_guard = test_util::http_server(); let (file_fetcher, _, _, http_cache) = setup_with_blob_store_and_cache(CacheSetting::Use, None); let specifier = resolve_url( "http://localhost:4550/REDIRECT/subdir/redirects/redirect1.js", ) .unwrap(); let redirected_specifier = resolve_url("http://localhost:4550/subdir/redirects/redirect1.js") .unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); assert_eq!(file.url, redirected_specifier); assert_eq!( get_text_from_cache(http_cache.as_ref(), &specifier), "", "redirected files should have empty cached contents" ); assert_eq!( get_location_header_from_cache(http_cache.as_ref(), &specifier), Some("/subdir/redirects/redirect1.js".to_string()), ); assert_eq!( get_text_from_cache(http_cache.as_ref(), &redirected_specifier), "export const redirect = 1;\n" ); assert_eq!( get_location_header_from_cache( http_cache.as_ref(), &redirected_specifier ), None ); } #[tokio::test] async fn test_fetch_no_remote() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); let location = temp_dir.path().join("remote").to_path_buf(); let file_fetcher = CliFileFetcher::new( Arc::new(GlobalHttpCache::new( location, crate::cache::RealDenoCacheEnv, )), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, false, CacheSetting::Use, log::Level::Info, ); let specifier = resolve_url("http://localhost:4545/run/002_hello.ts").unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_err()); let err = result.unwrap_err(); let err = err.downcast::().unwrap().into_kind(); match err { CliFetchNoFollowErrorKind::FetchNoFollow(err) => { let err = err.into_kind(); match &err { FetchNoFollowErrorKind::NoRemote { .. } => { assert_eq!(err.to_string(), "A remote specifier was requested: \"http://localhost:4545/run/002_hello.ts\", but --no-remote is specified."); } _ => unreachable!(), } } _ => unreachable!(), } } #[tokio::test] async fn test_fetch_cache_only() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); let location = temp_dir.path().join("remote").to_path_buf(); let file_fetcher_01 = CliFileFetcher::new( Arc::new(GlobalHttpCache::new(location.clone(), RealDenoCacheEnv)), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, true, CacheSetting::Only, log::Level::Info, ); let file_fetcher_02 = CliFileFetcher::new( Arc::new(GlobalHttpCache::new(location, RealDenoCacheEnv)), Arc::new(HttpClientProvider::new(None, None)), Default::default(), None, true, CacheSetting::Use, log::Level::Info, ); let specifier = resolve_url("http://localhost:4545/run/002_hello.ts").unwrap(); let result = file_fetcher_01.fetch_bypass_permissions(&specifier).await; assert!(result.is_err()); let err = result.unwrap_err(); let err = err.downcast::().unwrap().into_kind(); match err { CliFetchNoFollowErrorKind::FetchNoFollow(err) => { let err = err.into_kind(); match &err { FetchNoFollowErrorKind::NotCached { .. } => { assert_eq!(err.to_string(), "Specifier not found in cache: \"http://localhost:4545/run/002_hello.ts\", --cached-only is specified."); } _ => unreachable!(), } } _ => unreachable!(), } let result = file_fetcher_02.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let result = file_fetcher_01.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); } #[tokio::test] async fn test_fetch_local_bypasses_file_cache() { let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None); let fixture_path = temp_dir.path().join("mod.ts"); let specifier = ModuleSpecifier::from_file_path(&fixture_path).unwrap(); fixture_path.write(r#"console.log("hello deno");"#); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!(&*file.source, r#"console.log("hello deno");"#); fixture_path.write(r#"console.log("goodbye deno");"#); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = TextDecodedFile::decode(result.unwrap()).unwrap(); assert_eq!(&*file.source, r#"console.log("goodbye deno");"#); } #[tokio::test] async fn test_respect_cache_revalidates() { let _g = test_util::http_server(); let temp_dir = TempDir::new(); let (file_fetcher, _) = setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); let specifier = ModuleSpecifier::parse("http://localhost:4545/dynamic").unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let first = file.source; let (file_fetcher, _) = setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let second = file.source; assert_ne!(first, second); } #[tokio::test] async fn test_respect_cache_still_fresh() { let _g = test_util::http_server(); let temp_dir = TempDir::new(); let (file_fetcher, _) = setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); let specifier = ModuleSpecifier::parse("http://localhost:4545/dynamic_cache").unwrap(); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let first = file.source; let (file_fetcher, _) = setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let second = file.source; assert_eq!(first, second); } #[tokio::test] async fn test_fetch_local_utf_16be() { let expected = String::from_utf8(b"console.log(\"Hello World\");\x0A".to_vec()).unwrap(); test_fetch_local_encoded("utf-16be", expected).await; } #[tokio::test] async fn test_fetch_local_utf_16le() { let expected = String::from_utf8(b"console.log(\"Hello World\");\x0A".to_vec()).unwrap(); test_fetch_local_encoded("utf-16le", expected).await; } #[tokio::test] async fn test_fetch_local_utf8_with_bom() { let expected = String::from_utf8(b"console.log(\"Hello World\");\x0A".to_vec()).unwrap(); test_fetch_local_encoded("utf-8", expected).await; } #[tokio::test] async fn test_fetch_remote_utf16_le() { let expected = std::str::from_utf8(b"console.log(\"Hello World\");\x0A").unwrap(); test_fetch_remote_encoded("utf-16le.ts", "utf-16le", expected).await; } #[tokio::test] async fn test_fetch_remote_utf16_be() { let expected = std::str::from_utf8(b"console.log(\"Hello World\");\x0A").unwrap(); test_fetch_remote_encoded("utf-16be.ts", "utf-16be", expected).await; } #[tokio::test] async fn test_fetch_remote_window_1255() { let expected = "console.log(\"\u{5E9}\u{5DC}\u{5D5}\u{5DD} \ \u{5E2}\u{5D5}\u{5DC}\u{5DD}\");\u{A}"; test_fetch_remote_encoded("windows-1255", "windows-1255", expected).await; } fn create_http_client_adapter() -> HttpClientAdapter { HttpClientAdapter { http_client_provider: Arc::new(HttpClientProvider::new(None, None)), download_log_level: log::Level::Info, progress_bar: None, } } #[tokio::test] async fn test_fetch_string() { let _http_server_guard = test_util::http_server(); let url = Url::parse("http://127.0.0.1:4545/assets/fixture.json").unwrap(); let client = create_http_client_adapter(); let result = client.send_no_follow(&url, HeaderMap::new()).await; if let Ok(SendResponse::Success(headers, body)) = result { assert!(!body.is_empty()); assert_eq!(headers.get("content-type").unwrap(), "application/json"); assert_eq!(headers.get("etag"), None); assert_eq!(headers.get("x-typescript-types"), None); } else { panic!(); } } #[tokio::test] async fn test_fetch_gzip() { let _http_server_guard = test_util::http_server(); let url = Url::parse("http://127.0.0.1:4545/run/import_compression/gziped") .unwrap(); let client = create_http_client_adapter(); let result = client.send_no_follow(&url, HeaderMap::new()).await; if let Ok(SendResponse::Success(headers, body)) = result { assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')"); assert_eq!( headers.get("content-type").unwrap(), "application/javascript" ); assert_eq!(headers.get("etag"), None); assert_eq!(headers.get("x-typescript-types"), None); } else { panic!(); } } #[tokio::test] async fn test_fetch_with_etag() { let _http_server_guard = test_util::http_server(); let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap(); let client = create_http_client_adapter(); let result = client.send_no_follow(&url, HeaderMap::new()).await; if let Ok(SendResponse::Success(headers, body)) = result { assert!(!body.is_empty()); assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')"); assert_eq!( headers.get("content-type").unwrap(), "application/typescript" ); assert_eq!(headers.get("etag").unwrap(), "33a64df551425fcc55e"); } else { panic!(); } let mut headers = HeaderMap::new(); headers.insert("if-none-match", "33a64df551425fcc55e".parse().unwrap()); let res = client.send_no_follow(&url, headers).await; assert_eq!(res.unwrap(), SendResponse::NotModified); } #[tokio::test] async fn test_fetch_brotli() { let _http_server_guard = test_util::http_server(); let url = Url::parse("http://127.0.0.1:4545/run/import_compression/brotli") .unwrap(); let client = create_http_client_adapter(); let result = client.send_no_follow(&url, HeaderMap::new()).await; if let Ok(SendResponse::Success(headers, body)) = result { assert!(!body.is_empty()); assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');"); assert_eq!( headers.get("content-type").unwrap(), "application/javascript" ); assert_eq!(headers.get("etag"), None); assert_eq!(headers.get("x-typescript-types"), None); } else { panic!(); } } #[tokio::test] async fn test_fetch_accept() { let _http_server_guard = test_util::http_server(); let url = Url::parse("http://127.0.0.1:4545/echo_accept").unwrap(); let client = create_http_client_adapter(); let mut headers = HeaderMap::new(); headers.insert("accept", "application/json".parse().unwrap()); let result = client.send_no_follow(&url, headers).await; if let Ok(SendResponse::Success(_, body)) = result { assert_eq!(body, r#"{"accept":"application/json"}"#.as_bytes()); } else { panic!(); } } #[tokio::test] async fn test_fetch_no_follow_with_redirect() { let _http_server_guard = test_util::http_server(); let url = Url::parse("http://127.0.0.1:4546/assets/fixture.json").unwrap(); // Dns resolver substitutes `127.0.0.1` with `localhost` let target_url = Url::parse("http://localhost:4545/assets/fixture.json").unwrap(); let client = create_http_client_adapter(); let result = client.send_no_follow(&url, Default::default()).await; if let Ok(SendResponse::Redirect(headers)) = result { assert_eq!(headers.get("location").unwrap(), target_url.as_str()); } else { panic!(); } } #[tokio::test] async fn server_error() { let _g = test_util::http_server(); let url_str = "http://127.0.0.1:4545/server_error"; let url = Url::parse(url_str).unwrap(); let client = create_http_client_adapter(); let result = client.send_no_follow(&url, Default::default()).await; if let Err(SendError::StatusCode(status)) = result { assert_eq!(status, 500); } else { panic!("{:?}", result); } } #[tokio::test] async fn request_error() { let _g = test_util::http_server(); let url_str = "http://127.0.0.1:9999/"; let url = Url::parse(url_str).unwrap(); let client = create_http_client_adapter(); let result = client.send_no_follow(&url, Default::default()).await; assert!(matches!(result, Err(SendError::Failed(_)))); } #[track_caller] fn get_text_from_cache( http_cache: &dyn HttpCache, url: &ModuleSpecifier, ) -> String { let cache_key = http_cache.cache_item_key(url).unwrap(); let bytes = http_cache.get(&cache_key, None).unwrap().unwrap().content; String::from_utf8(bytes.into_owned()).unwrap() } #[track_caller] fn get_location_header_from_cache( http_cache: &dyn HttpCache, url: &ModuleSpecifier, ) -> Option { let cache_key = http_cache.cache_item_key(url).unwrap(); http_cache .read_headers(&cache_key) .unwrap() .unwrap() .remove("location") } }