turbo_tasks_fetch/
error.rs

1use anyhow::Result;
2use turbo_rcstr::{RcStr, rcstr};
3use turbo_tasks::{ResolvedVc, Vc};
4use turbo_tasks_fs::FileSystemPath;
5use turbopack_core::issue::{Issue, IssueSeverity, IssueStage, OptionStyledString, StyledString};
6
7#[derive(Debug)]
8#[turbo_tasks::value(shared)]
9pub enum FetchErrorKind {
10    Connect {
11        has_system_certs: bool,
12        has_rustls_cause: bool,
13    },
14    Timeout,
15    Status(u16),
16    Other,
17}
18
19#[turbo_tasks::value(shared)]
20pub struct FetchError {
21    pub url: ResolvedVc<RcStr>,
22    pub kind: ResolvedVc<FetchErrorKind>,
23    pub detail: ResolvedVc<StyledString>,
24}
25
26/// Attempt to determine if there's a `rustls::Error` in the error's source chain.
27///
28/// This logic is fragile (e.g. depends that our copy of rustls and the version that reqwest uses
29/// match exactly), but it's covered by unit tests. This seems slightly better than using `Display`
30/// or `Debug` and inspecting the string.
31fn has_rustls_cause(err: &reqwest::Error) -> bool {
32    // make sure this cfg matches the one in `Cargo.toml`!
33    #[cfg(not(any(
34        all(target_os = "windows", target_arch = "aarch64"),
35        target_arch = "wasm32"
36    )))]
37    {
38        let mut source = std::error::Error::source(err);
39        while let Some(err) = source {
40            if err.downcast_ref::<rustls::Error>().is_some() {
41                return true;
42            }
43            if let Some(err) = err.downcast_ref::<std::io::Error>() {
44                // `std::io::Error`'s `source` implementation returns the source of the wrapped
45                // error instead of the wrapped error itself, so we need to special-case this,
46                // otherwise we risk skipping over the rustls error.
47                source = err.get_ref().map(|e| e as &dyn std::error::Error);
48            } else {
49                source = std::error::Error::source(err);
50            }
51        }
52        return false;
53    };
54
55    // uses native-tls
56    #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
57    return false;
58}
59
60impl FetchError {
61    pub(crate) fn from_reqwest_error(
62        error: &reqwest::Error,
63        url: &str,
64        webpki_certs_only: bool,
65    ) -> FetchError {
66        let kind = if error.is_connect() {
67            FetchErrorKind::Connect {
68                has_system_certs: webpki_certs_only,
69                has_rustls_cause: has_rustls_cause(error),
70            }
71        } else if error.is_timeout() {
72            FetchErrorKind::Timeout
73        } else if let Some(status) = error.status() {
74            FetchErrorKind::Status(status.as_u16())
75        } else {
76            FetchErrorKind::Other
77        };
78
79        FetchError {
80            detail: StyledString::Text(error.to_string().into()).resolved_cell(),
81            url: ResolvedVc::cell(url.into()),
82            kind: kind.resolved_cell(),
83        }
84    }
85}
86
87#[turbo_tasks::value_impl]
88impl FetchError {
89    #[turbo_tasks::function]
90    pub fn to_issue(
91        &self,
92        severity: IssueSeverity,
93        issue_context: FileSystemPath,
94    ) -> Vc<FetchIssue> {
95        FetchIssue {
96            issue_context,
97            severity,
98            url: self.url,
99            kind: self.kind,
100            detail: self.detail,
101        }
102        .cell()
103    }
104}
105
106#[turbo_tasks::value(shared)]
107pub struct FetchIssue {
108    pub issue_context: FileSystemPath,
109    pub severity: IssueSeverity,
110    pub url: ResolvedVc<RcStr>,
111    pub kind: ResolvedVc<FetchErrorKind>,
112    pub detail: ResolvedVc<StyledString>,
113}
114
115#[turbo_tasks::value_impl]
116impl Issue for FetchIssue {
117    #[turbo_tasks::function]
118    fn file_path(&self) -> Vc<FileSystemPath> {
119        self.issue_context.clone().cell()
120    }
121
122    fn severity(&self) -> IssueSeverity {
123        self.severity
124    }
125
126    #[turbo_tasks::function]
127    fn title(&self) -> Vc<StyledString> {
128        StyledString::Text(rcstr!("Error while requesting resource")).cell()
129    }
130
131    #[turbo_tasks::function]
132    fn stage(&self) -> Vc<IssueStage> {
133        IssueStage::Load.into()
134    }
135
136    #[turbo_tasks::function]
137    async fn description(&self) -> Result<Vc<OptionStyledString>> {
138        let url = &*self.url.await?;
139        let kind = &*self.kind.await?;
140
141        Ok(Vc::cell(Some(
142            match kind {
143                FetchErrorKind::Connect {
144                    has_system_certs,
145                    has_rustls_cause,
146                } => {
147                    let base_message = StyledString::Line(vec![
148                        StyledString::Text(rcstr!(
149                            "There was an issue establishing a connection while requesting "
150                        )),
151                        StyledString::Code(url.clone()),
152                    ]);
153                    if !*has_system_certs && *has_rustls_cause {
154                        StyledString::Stack(vec![
155                            base_message,
156                            StyledString::Line(vec![
157                                StyledString::Strong(rcstr!("Hint: ")),
158                                StyledString::Text(rcstr!(
159                                    "It looks like this error was TLS-related. Try enabling \
160                                     system TLS certificates with "
161                                )),
162                                StyledString::Code(rcstr!(
163                                    "NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1"
164                                )),
165                                StyledString::Text(rcstr!(" as an environment variable, or set ")),
166                                StyledString::Code(rcstr!(
167                                    "experimental.turbopackUseSystemTlsCerts"
168                                )),
169                                StyledString::Text(rcstr!(" in your ")),
170                                StyledString::Code(rcstr!("next.config.js")),
171                                StyledString::Text(rcstr!(" file.")),
172                            ]),
173                        ])
174                    } else {
175                        base_message
176                    }
177                }
178                FetchErrorKind::Status(status) => StyledString::Line(vec![
179                    StyledString::Text(rcstr!("Received response with status ")),
180                    StyledString::Code(RcStr::from(status.to_string())),
181                    StyledString::Text(rcstr!(" when requesting ")),
182                    StyledString::Code(url.clone()),
183                ]),
184                FetchErrorKind::Timeout => StyledString::Line(vec![
185                    StyledString::Text(rcstr!("Connection timed out when requesting ")),
186                    StyledString::Code(url.clone()),
187                ]),
188                FetchErrorKind::Other => StyledString::Line(vec![
189                    StyledString::Text(rcstr!("There was an issue requesting ")),
190                    StyledString::Code(url.clone()),
191                ]),
192            }
193            .resolved_cell(),
194        )))
195    }
196
197    #[turbo_tasks::function]
198    fn detail(&self) -> Vc<OptionStyledString> {
199        Vc::cell(Some(self.detail))
200    }
201}