1
0
Fork 0
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:
Yusuke Tanaka 2024-08-08 15:18:33 +09:00 committed by GitHub
parent 9d6da1036d
commit 4e4c96bf66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 98 additions and 34 deletions

7
Cargo.lock generated
View file

@ -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"

View file

@ -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> {

View file

@ -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();

View file

@ -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

View file

@ -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()))
} }
} }

View file

@ -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]

View file

@ -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])

View file

@ -1976,14 +1976,17 @@ Deno.test(
}, },
}); });
const err = await assertRejects(() => const url = `http://localhost:${listenPort}/`;
fetch(`http://localhost:${listenPort}/`, { const err = await assertRejects(
body: stream, () =>
method: "POST", fetch(url, {
}) body: stream,
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: ",
);
},
);