mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
fix(ext/fetch): include URL and error details on fetch failures (#24910)
This commit improves error messages that `fetch` generates on failure. Fixes #24835
This commit is contained in:
parent
9d6da1036d
commit
4e4c96bf66
8 changed files with 98 additions and 34 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -1493,6 +1493,7 @@ dependencies = [
|
||||||
"deno_permissions",
|
"deno_permissions",
|
||||||
"deno_tls",
|
"deno_tls",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
|
"error_reporter",
|
||||||
"fast-socks5",
|
"fast-socks5",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
@ -2698,6 +2699,12 @@ version = "3.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
|
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "error_reporter"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "escape8259"
|
name = "escape8259"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|
|
@ -390,10 +390,10 @@ impl HttpClient {
|
||||||
let response = match self.client.clone().send(request).await {
|
let response = match self.client.clone().send(request).await {
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if is_error_connect(&err) {
|
if err.is_connect_error() {
|
||||||
return Ok(FetchOnceResult::RequestError(err.to_string()));
|
return Ok(FetchOnceResult::RequestError(err.to_string()));
|
||||||
}
|
}
|
||||||
return Err(err);
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -531,7 +531,7 @@ impl HttpClient {
|
||||||
.clone()
|
.clone()
|
||||||
.send(req)
|
.send(req)
|
||||||
.await
|
.await
|
||||||
.map_err(DownloadError::Fetch)?;
|
.map_err(|e| DownloadError::Fetch(e.into()))?;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status.is_redirection() {
|
if status.is_redirection() {
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
|
@ -551,7 +551,7 @@ impl HttpClient {
|
||||||
.clone()
|
.clone()
|
||||||
.send(req)
|
.send(req)
|
||||||
.await
|
.await
|
||||||
.map_err(DownloadError::Fetch)?;
|
.map_err(|e| DownloadError::Fetch(e.into()))?;
|
||||||
let status = new_response.status();
|
let status = new_response.status();
|
||||||
if status.is_redirection() {
|
if status.is_redirection() {
|
||||||
response = new_response;
|
response = new_response;
|
||||||
|
@ -567,13 +567,6 @@ impl HttpClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_error_connect(err: &AnyError) -> bool {
|
|
||||||
err
|
|
||||||
.downcast_ref::<hyper_util::client::legacy::Error>()
|
|
||||||
.map(|err| err.is_connect())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_response_body_with_progress(
|
async fn get_response_body_with_progress(
|
||||||
response: http::Response<deno_fetch::ResBody>,
|
response: http::Response<deno_fetch::ResBody>,
|
||||||
progress_guard: Option<&UpdateGuard>,
|
progress_guard: Option<&UpdateGuard>,
|
||||||
|
@ -685,7 +678,7 @@ impl RequestBuilder {
|
||||||
pub async fn send(
|
pub async fn send(
|
||||||
self,
|
self,
|
||||||
) -> Result<http::Response<deno_fetch::ResBody>, AnyError> {
|
) -> Result<http::Response<deno_fetch::ResBody>, AnyError> {
|
||||||
self.client.send(self.req).await
|
self.client.send(self.req).await.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> http::Request<deno_fetch::ReqBody> {
|
pub fn build(self) -> http::Request<deno_fetch::ReqBody> {
|
||||||
|
|
|
@ -65,7 +65,7 @@ const REQUEST_BODY_HEADER_NAMES = [
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} rid
|
* @param {number} rid
|
||||||
* @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number, error: string? }>}
|
* @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number, error: [string, string]? }>}
|
||||||
*/
|
*/
|
||||||
function opFetchSend(rid) {
|
function opFetchSend(rid) {
|
||||||
return op_fetch_send(rid);
|
return op_fetch_send(rid);
|
||||||
|
@ -177,8 +177,9 @@ async function mainFetch(req, recursive, terminator) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Re-throw any body errors
|
// Re-throw any body errors
|
||||||
if (resp.error) {
|
if (resp.error !== null) {
|
||||||
throw new TypeError("body failed", { cause: new Error(resp.error) });
|
const { 0: message, 1: cause } = resp.error;
|
||||||
|
throw new TypeError(message, { cause: new Error(cause) });
|
||||||
}
|
}
|
||||||
if (terminator.aborted) return abortedNetworkError();
|
if (terminator.aborted) return abortedNetworkError();
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ deno_core.workspace = true
|
||||||
deno_permissions.workspace = true
|
deno_permissions.workspace = true
|
||||||
deno_tls.workspace = true
|
deno_tls.workspace = true
|
||||||
dyn-clone = "1"
|
dyn-clone = "1"
|
||||||
|
error_reporter = "1"
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
http-body-util.workspace = true
|
http-body-util.workspace = true
|
||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
|
|
|
@ -26,6 +26,7 @@ use deno_core::futures::Future;
|
||||||
use deno_core::futures::FutureExt;
|
use deno_core::futures::FutureExt;
|
||||||
use deno_core::futures::Stream;
|
use deno_core::futures::Stream;
|
||||||
use deno_core::futures::StreamExt;
|
use deno_core::futures::StreamExt;
|
||||||
|
use deno_core::futures::TryFutureExt;
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
use deno_core::unsync::spawn;
|
use deno_core::unsync::spawn;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
|
@ -429,7 +430,7 @@ where
|
||||||
|
|
||||||
let mut request = http::Request::new(body);
|
let mut request = http::Request::new(body);
|
||||||
*request.method_mut() = method.clone();
|
*request.method_mut() = method.clone();
|
||||||
*request.uri_mut() = uri;
|
*request.uri_mut() = uri.clone();
|
||||||
|
|
||||||
if let Some((username, password)) = maybe_authority {
|
if let Some((username, password)) = maybe_authority {
|
||||||
request.headers_mut().insert(
|
request.headers_mut().insert(
|
||||||
|
@ -469,8 +470,15 @@ where
|
||||||
let cancel_handle = CancelHandle::new_rc();
|
let cancel_handle = CancelHandle::new_rc();
|
||||||
let cancel_handle_ = cancel_handle.clone();
|
let cancel_handle_ = cancel_handle.clone();
|
||||||
|
|
||||||
let fut =
|
let fut = {
|
||||||
async move { client.send(request).or_cancel(cancel_handle_).await };
|
async move {
|
||||||
|
client
|
||||||
|
.send(request)
|
||||||
|
.map_err(Into::into)
|
||||||
|
.or_cancel(cancel_handle_)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let request_rid = state.resource_table.add(FetchRequestResource {
|
let request_rid = state.resource_table.add(FetchRequestResource {
|
||||||
future: Box::pin(fut),
|
future: Box::pin(fut),
|
||||||
|
@ -532,7 +540,11 @@ pub struct FetchResponse {
|
||||||
pub content_length: Option<u64>,
|
pub content_length: Option<u64>,
|
||||||
pub remote_addr_ip: Option<String>,
|
pub remote_addr_ip: Option<String>,
|
||||||
pub remote_addr_port: Option<u16>,
|
pub remote_addr_port: Option<u16>,
|
||||||
pub error: Option<String>,
|
/// This field is populated if some error occurred which needs to be
|
||||||
|
/// reconstructed in the JS side to set the error _cause_.
|
||||||
|
/// In the tuple, the first element is an error message and the second one is
|
||||||
|
/// an error cause.
|
||||||
|
pub error: Option<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2(async)]
|
#[op2(async)]
|
||||||
|
@ -558,16 +570,16 @@ pub async fn op_fetch_send(
|
||||||
// reconstruct an error chain (eg: `new TypeError(..., { cause: new Error(...) })`).
|
// reconstruct an error chain (eg: `new TypeError(..., { cause: new Error(...) })`).
|
||||||
// TODO(mmastrac): it would be a lot easier if we just passed a v8::Global through here instead
|
// TODO(mmastrac): it would be a lot easier if we just passed a v8::Global through here instead
|
||||||
let mut err_ref: &dyn std::error::Error = err.as_ref();
|
let mut err_ref: &dyn std::error::Error = err.as_ref();
|
||||||
while let Some(err) = std::error::Error::source(err_ref) {
|
while let Some(err_src) = std::error::Error::source(err_ref) {
|
||||||
if let Some(err) = err.downcast_ref::<hyper::Error>() {
|
if let Some(err_src) = err_src.downcast_ref::<hyper::Error>() {
|
||||||
if let Some(err) = std::error::Error::source(err) {
|
if let Some(err_src) = std::error::Error::source(err_src) {
|
||||||
return Ok(FetchResponse {
|
return Ok(FetchResponse {
|
||||||
error: Some(err.to_string()),
|
error: Some((err.to_string(), err_src.to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err_ref = err;
|
err_ref = err_src;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(type_error(err.to_string()));
|
return Err(type_error(err.to_string()));
|
||||||
|
@ -1082,11 +1094,41 @@ type Connector = proxy::ProxyConnector<HttpConnector>;
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
const STAR_STAR: HeaderValue = HeaderValue::from_static("*/*");
|
const STAR_STAR: HeaderValue = HeaderValue::from_static("*/*");
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ClientSendError {
|
||||||
|
uri: Uri,
|
||||||
|
source: hyper_util::client::legacy::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientSendError {
|
||||||
|
pub fn is_connect_error(&self) -> bool {
|
||||||
|
self.source.is_connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ClientSendError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"error sending request for url ({uri}): {source}",
|
||||||
|
uri = self.uri,
|
||||||
|
// NOTE: we can use `std::error::Report` instead once it's stabilized.
|
||||||
|
source = error_reporter::Report::new(&self.source),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ClientSendError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
Some(&self.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub async fn send(
|
pub async fn send(
|
||||||
self,
|
self,
|
||||||
mut req: http::Request<ReqBody>,
|
mut req: http::Request<ReqBody>,
|
||||||
) -> Result<http::Response<ResBody>, AnyError> {
|
) -> Result<http::Response<ResBody>, ClientSendError> {
|
||||||
req
|
req
|
||||||
.headers_mut()
|
.headers_mut()
|
||||||
.entry(USER_AGENT)
|
.entry(USER_AGENT)
|
||||||
|
@ -1098,7 +1140,13 @@ impl Client {
|
||||||
req.headers_mut().insert(PROXY_AUTHORIZATION, auth.clone());
|
req.headers_mut().insert(PROXY_AUTHORIZATION, auth.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = self.inner.oneshot(req).await?;
|
let uri = req.uri().clone();
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.inner
|
||||||
|
.oneshot(req)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientSendError { uri, source: e })?;
|
||||||
Ok(resp.map(|b| b.map_err(|e| anyhow!(e)).boxed()))
|
Ok(resp.map(|b| b.map_err(|e| anyhow!(e)).boxed()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
DANGER: TLS certificate validation is disabled for: deno.land
|
DANGER: TLS certificate validation is disabled for: deno.land
|
||||||
error: Import 'https://localhost:5545/subdir/mod2.ts' failed: client error[WILDCARD]
|
error: Import 'https://localhost:5545/subdir/mod2.ts' failed: error sending request for url (https://localhost:5545/subdir/mod2.ts): client error[WILDCARD]
|
||||||
at file:///[WILDCARD]/cafile_url_imports.ts:[WILDCARD]
|
at file:///[WILDCARD]/cafile_url_imports.ts:[WILDCARD]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
error: Uncaught (in promise) TypeError: client error[WILDCARD]
|
error: Uncaught (in promise) TypeError: error sending request for url (https://nonexistent.deno.land/): client error[WILDCARD]
|
||||||
await fetch("https://nonexistent.deno.land/");
|
await fetch("https://nonexistent.deno.land/");
|
||||||
^[WILDCARD]
|
^[WILDCARD]
|
||||||
at async fetch (ext:[WILDCARD])
|
at async fetch (ext:[WILDCARD])
|
||||||
|
|
|
@ -1976,14 +1976,17 @@ Deno.test(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const err = await assertRejects(() =>
|
const url = `http://localhost:${listenPort}/`;
|
||||||
fetch(`http://localhost:${listenPort}/`, {
|
const err = await assertRejects(
|
||||||
|
() =>
|
||||||
|
fetch(url, {
|
||||||
body: stream,
|
body: stream,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
}),
|
||||||
|
TypeError,
|
||||||
|
`error sending request for url (${url}): client error (SendRequest): error from user's Body stream`,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(err instanceof TypeError, `err was not a TypeError ${err}`);
|
|
||||||
assert(err.cause, `err.cause was null ${err}`);
|
assert(err.cause, `err.cause was null ${err}`);
|
||||||
assert(
|
assert(
|
||||||
err.cause instanceof Error,
|
err.cause instanceof Error,
|
||||||
|
@ -2060,3 +2063,14 @@ Deno.test("URL authority is used as 'Authorization' header", async () => {
|
||||||
await server.finished;
|
await server.finished;
|
||||||
assertEquals(authHeader, "Basic ZGVubzpsYW5k");
|
assertEquals(authHeader, "Basic ZGVubzpsYW5k");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { net: true } },
|
||||||
|
async function errorMessageIncludesUrlAndDetails() {
|
||||||
|
await assertRejects(
|
||||||
|
() => fetch("http://example.invalid"),
|
||||||
|
TypeError,
|
||||||
|
"error sending request for url (http://example.invalid/): client error (Connect): dns error: ",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
Loading…
Reference in a new issue