mirror of
https://github.com/denoland/deno.git
synced 2025-08-04 10:59:13 +00:00
chore: fix recent regression with deno upgrade
not handling redirects (#17045)
This commit is contained in:
parent
f9db129bdf
commit
4a64ca8501
5 changed files with 713 additions and 617 deletions
|
@ -4,6 +4,7 @@ use crate::args::CacheSetting;
|
|||
use crate::auth_tokens::AuthTokens;
|
||||
use crate::cache::HttpCache;
|
||||
use crate::colors;
|
||||
use crate::http_util::resolve_redirect_from_response;
|
||||
use crate::http_util::CacheSemantics;
|
||||
use crate::http_util::FetchOnceArgs;
|
||||
use crate::http_util::FetchOnceResult;
|
||||
|
@ -21,6 +22,11 @@ use deno_core::futures;
|
|||
use deno_core::futures::future::FutureExt;
|
||||
use deno_core::parking_lot::Mutex;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use deno_runtime::deno_fetch::reqwest::header::HeaderValue;
|
||||
use deno_runtime::deno_fetch::reqwest::header::ACCEPT;
|
||||
use deno_runtime::deno_fetch::reqwest::header::AUTHORIZATION;
|
||||
use deno_runtime::deno_fetch::reqwest::header::IF_NONE_MATCH;
|
||||
use deno_runtime::deno_fetch::reqwest::StatusCode;
|
||||
use deno_runtime::deno_web::BlobStore;
|
||||
use deno_runtime::permissions::Permissions;
|
||||
use log::debug;
|
||||
|
@ -457,14 +463,16 @@ impl FileFetcher {
|
|||
let file_fetcher = self.clone();
|
||||
// A single pass of fetch either yields code or yields a redirect.
|
||||
async move {
|
||||
match client
|
||||
.fetch_once(FetchOnceArgs {
|
||||
match fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url: specifier.clone(),
|
||||
maybe_accept: maybe_accept.clone(),
|
||||
maybe_etag,
|
||||
maybe_auth_token,
|
||||
})
|
||||
.await?
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
FetchOnceResult::NotModified => {
|
||||
let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap();
|
||||
|
@ -627,17 +635,99 @@ impl FileFetcher {
|
|||
}
|
||||
}
|
||||
|
||||
/// Asynchronously fetches the given HTTP URL one pass only.
|
||||
/// If no redirect is present and no error occurs,
|
||||
/// yields Code(ResultPayload).
|
||||
/// If redirect occurs, does not follow and
|
||||
/// yields Redirect(url).
|
||||
async fn fetch_once(
|
||||
http_client: &HttpClient,
|
||||
args: FetchOnceArgs,
|
||||
) -> Result<FetchOnceResult, AnyError> {
|
||||
let mut request = http_client.get_no_redirect(args.url.clone());
|
||||
|
||||
if let Some(etag) = args.maybe_etag {
|
||||
let if_none_match_val = HeaderValue::from_str(&etag)?;
|
||||
request = request.header(IF_NONE_MATCH, if_none_match_val);
|
||||
}
|
||||
if let Some(auth_token) = args.maybe_auth_token {
|
||||
let authorization_val = HeaderValue::from_str(&auth_token.to_string())?;
|
||||
request = request.header(AUTHORIZATION, authorization_val);
|
||||
}
|
||||
if let Some(accept) = args.maybe_accept {
|
||||
let accepts_val = HeaderValue::from_str(&accept)?;
|
||||
request = request.header(ACCEPT, accepts_val);
|
||||
}
|
||||
let response = request.send().await?;
|
||||
|
||||
if response.status() == StatusCode::NOT_MODIFIED {
|
||||
return Ok(FetchOnceResult::NotModified);
|
||||
}
|
||||
|
||||
let mut result_headers = HashMap::new();
|
||||
let response_headers = response.headers();
|
||||
|
||||
if let Some(warning) = response_headers.get("X-Deno-Warning") {
|
||||
log::warn!(
|
||||
"{} {}",
|
||||
crate::colors::yellow("Warning"),
|
||||
warning.to_str().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
for key in response_headers.keys() {
|
||||
let key_str = key.to_string();
|
||||
let values = response_headers.get_all(key);
|
||||
let values_str = values
|
||||
.iter()
|
||||
.map(|e| e.to_str().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
result_headers.insert(key_str, values_str);
|
||||
}
|
||||
|
||||
if response.status().is_redirection() {
|
||||
let new_url = resolve_redirect_from_response(&args.url, &response)?;
|
||||
return Ok(FetchOnceResult::Redirect(new_url, result_headers));
|
||||
}
|
||||
|
||||
if response.status().is_client_error() || response.status().is_server_error()
|
||||
{
|
||||
let err = if response.status() == StatusCode::NOT_FOUND {
|
||||
custom_error(
|
||||
"NotFound",
|
||||
format!("Import '{}' failed, not found.", args.url),
|
||||
)
|
||||
} else {
|
||||
generic_error(format!(
|
||||
"Import '{}' failed: {}",
|
||||
args.url,
|
||||
response.status()
|
||||
))
|
||||
};
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let body = response.bytes().await?.to_vec();
|
||||
|
||||
Ok(FetchOnceResult::Code(body, result_headers))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cache::CachedUrlMetadata;
|
||||
use crate::http_util::HttpClient;
|
||||
use crate::version;
|
||||
|
||||
use super::*;
|
||||
use deno_core::error::get_custom_error_class;
|
||||
use deno_core::resolve_url;
|
||||
use deno_core::resolve_url_or_path;
|
||||
use deno_core::url::Url;
|
||||
use deno_runtime::deno_fetch::create_http_client;
|
||||
use deno_runtime::deno_web::Blob;
|
||||
use deno_runtime::deno_web::InMemoryBlobPart;
|
||||
use std::fs::read;
|
||||
use test_util::TempDir;
|
||||
|
||||
fn setup(
|
||||
|
@ -1640,4 +1730,480 @@ mod tests {
|
|||
\u{5E2}\u{5D5}\u{5DC}\u{5DD}\");\u{A}";
|
||||
test_fetch_remote_encoded("windows-1255", "windows-1255", expected).await;
|
||||
}
|
||||
|
||||
fn create_test_client() -> HttpClient {
|
||||
HttpClient::from_client(
|
||||
create_http_client(
|
||||
"test_client".to_string(),
|
||||
None,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_string() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server. See target/debug/test_server
|
||||
let url = Url::parse("http://127.0.0.1:4545/assets/fixture.json").unwrap();
|
||||
let client = create_test_client();
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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();
|
||||
// Relies on external http server. See target/debug/test_server
|
||||
let url = Url::parse("http://127.0.0.1:4545/run/import_compression/gziped")
|
||||
.unwrap();
|
||||
let client = create_test_client();
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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_test_client();
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url: url.clone(),
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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 res = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: Some("33a64df551425fcc55e".to_string()),
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_brotli() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server. See target/debug/test_server
|
||||
let url = Url::parse("http://127.0.0.1:4545/run/import_compression/brotli")
|
||||
.unwrap();
|
||||
let client = create_test_client();
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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();
|
||||
// Relies on external http server. See target/debug/test_server
|
||||
let url = Url::parse("http://127.0.0.1:4545/echo_accept").unwrap();
|
||||
let client = create_test_client();
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: Some("application/json".to_string()),
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, _)) = result {
|
||||
assert_eq!(body, r#"{"accept":"application/json"}"#.as_bytes());
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_once_with_redirect() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server. See target/debug/test_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_test_client();
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Redirect(url, _)) = result {
|
||||
assert_eq!(url, target_url);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_with_cafile_string() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server. See target/debug/test_server
|
||||
let url = Url::parse("https://localhost:5545/assets/fixture.json").unwrap();
|
||||
|
||||
let client = HttpClient::from_client(
|
||||
create_http_client(
|
||||
version::get_user_agent(),
|
||||
None,
|
||||
vec![read(
|
||||
test_util::testdata_path()
|
||||
.join("tls/RootCA.pem")
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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_with_default_certificate_store() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server with a valid mozilla root CA cert.
|
||||
let url = Url::parse("https://deno.land").unwrap();
|
||||
let client = HttpClient::from_client(
|
||||
create_http_client(
|
||||
version::get_user_agent(),
|
||||
None, // This will load mozilla certs by default
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
println!("{:?}", result);
|
||||
if let Ok(FetchOnceResult::Code(body, _headers)) = result {
|
||||
assert!(!body.is_empty());
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(@justinmchase): Windows should verify certs too and fail to make this request without ca certs
|
||||
#[cfg(not(windows))]
|
||||
#[tokio::test]
|
||||
#[ignore] // https://github.com/denoland/deno/issues/12561
|
||||
async fn test_fetch_with_empty_certificate_store() {
|
||||
use deno_runtime::deno_tls::rustls::RootCertStore;
|
||||
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server with a valid mozilla root CA cert.
|
||||
let url = Url::parse("https://deno.land").unwrap();
|
||||
let client = HttpClient::new(
|
||||
Some(RootCertStore::empty()), // no certs loaded at all
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(FetchOnceResult::Code(_body, _headers)) = result {
|
||||
// This test is expected to fail since to CA certs have been loaded
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_with_cafile_gzip() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server. See target/debug/test_server
|
||||
let url =
|
||||
Url::parse("https://localhost:5545/run/import_compression/gziped")
|
||||
.unwrap();
|
||||
let client = HttpClient::from_client(
|
||||
create_http_client(
|
||||
version::get_user_agent(),
|
||||
None,
|
||||
vec![read(
|
||||
test_util::testdata_path()
|
||||
.join("tls/RootCA.pem")
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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_cafile_with_etag() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
let url = Url::parse("https://localhost:5545/etag_script.ts").unwrap();
|
||||
let client = HttpClient::from_client(
|
||||
create_http_client(
|
||||
version::get_user_agent(),
|
||||
None,
|
||||
vec![read(
|
||||
test_util::testdata_path()
|
||||
.join("tls/RootCA.pem")
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url: url.clone(),
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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");
|
||||
assert_eq!(headers.get("x-typescript-types"), None);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
|
||||
let res = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: Some("33a64df551425fcc55e".to_string()),
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_with_cafile_brotli() {
|
||||
let _http_server_guard = test_util::http_server();
|
||||
// Relies on external http server. See target/debug/test_server
|
||||
let url =
|
||||
Url::parse("https://localhost:5545/run/import_compression/brotli")
|
||||
.unwrap();
|
||||
let client = HttpClient::from_client(
|
||||
create_http_client(
|
||||
version::get_user_agent(),
|
||||
None,
|
||||
vec![read(
|
||||
test_util::testdata_path()
|
||||
.join("tls/RootCA.pem")
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(FetchOnceResult::Code(body, headers)) = 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 bad_redirect() {
|
||||
let _g = test_util::http_server();
|
||||
let url_str = "http://127.0.0.1:4545/bad_redirect";
|
||||
let url = Url::parse(url_str).unwrap();
|
||||
let client = create_test_client();
|
||||
let result = fetch_once(
|
||||
&client,
|
||||
FetchOnceArgs {
|
||||
url,
|
||||
maybe_accept: None,
|
||||
maybe_etag: None,
|
||||
maybe_auth_token: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
// Check that the error message contains the original URL
|
||||
assert!(err.to_string().contains(url_str));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue