golem_wasi_http/
error.rs

1#![cfg_attr(target_arch = "wasm32", allow(unused))]
2use std::error::Error as StdError;
3use std::fmt;
4use std::io;
5
6use crate::{StatusCode, Url};
7
8/// A `Result` alias where the `Err` case is `crate::Error`.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// The Errors that may occur when processing a `Request`.
12///
13/// Note: Errors may include the full URL used to make the `Request`. If the URL
14/// contains sensitive information (e.g. an API key as a query parameter), be
15/// sure to remove it ([`without_url`](Error::without_url))
16pub struct Error {
17    inner: Box<Inner>,
18}
19
20pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
21
22struct Inner {
23    kind: Kind,
24    source: Option<BoxError>,
25    url: Option<Url>,
26}
27
28impl Error {
29    pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
30    where
31        E: Into<BoxError>,
32    {
33        Error {
34            inner: Box::new(Inner {
35                kind,
36                source: source.map(Into::into),
37                url: None,
38            }),
39        }
40    }
41
42    /// Returns a possible URL related to this error.
43    pub fn url(&self) -> Option<&Url> {
44        self.inner.url.as_ref()
45    }
46
47    /// Returns a mutable reference to the URL related to this error
48    ///
49    /// This is useful if you need to remove sensitive information from the URL
50    /// (e.g. an API key in the query), but do not want to remove the URL
51    /// entirely.
52    pub fn url_mut(&mut self) -> Option<&mut Url> {
53        self.inner.url.as_mut()
54    }
55
56    /// Add a url related to this error (overwriting any existing)
57    pub fn with_url(mut self, url: Url) -> Self {
58        self.inner.url = Some(url);
59        self
60    }
61
62    /// Strip the related url from this error (if, for example, it contains
63    /// sensitive information)
64    pub fn without_url(mut self) -> Self {
65        self.inner.url = None;
66        self
67    }
68
69    /// Returns true if the error is from a type Builder.
70    pub fn is_builder(&self) -> bool {
71        matches!(self.inner.kind, Kind::Builder)
72    }
73
74    /// Returns true if the error is from a `RedirectPolicy`.
75    pub fn is_redirect(&self) -> bool {
76        matches!(self.inner.kind, Kind::Redirect)
77    }
78
79    /// Returns true if the error is from `Response::error_for_status`.
80    pub fn is_status(&self) -> bool {
81        matches!(self.inner.kind, Kind::Status(_))
82    }
83
84    /// Returns true if the error is related to a timeout.
85    pub fn is_timeout(&self) -> bool {
86        let mut source = self.source();
87
88        while let Some(err) = source {
89            if err.is::<TimedOut>() {
90                return true;
91            }
92            if let Some(io) = err.downcast_ref::<io::Error>() {
93                if io.kind() == io::ErrorKind::TimedOut {
94                    return true;
95                }
96            }
97            source = err.source();
98        }
99
100        false
101    }
102
103    /// Returns true if the error is related to the request
104    pub fn is_request(&self) -> bool {
105        matches!(self.inner.kind, Kind::Request)
106    }
107
108    /// Returns true if the error is related to the request or response body
109    pub fn is_body(&self) -> bool {
110        matches!(self.inner.kind, Kind::Body)
111    }
112
113    /// Returns true if the error is related to decoding the response's body
114    pub fn is_decode(&self) -> bool {
115        matches!(self.inner.kind, Kind::Decode)
116    }
117
118    /// Returns the status code, if the error was generated from a response.
119    pub fn status(&self) -> Option<StatusCode> {
120        match self.inner.kind {
121            Kind::Status(code) => Some(code),
122            _ => None,
123        }
124    }
125
126    // private
127
128    #[allow(unused)]
129    pub(crate) fn into_io(self) -> io::Error {
130        io::Error::other(self)
131    }
132}
133
134impl fmt::Debug for Error {
135    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136        let mut builder = f.debug_struct("golem_wasi_http::Error");
137
138        builder.field("kind", &self.inner.kind);
139
140        if let Some(ref url) = self.inner.url {
141            builder.field("url", &url.as_str());
142        }
143        if let Some(ref source) = self.inner.source {
144            builder.field("source", source);
145        }
146
147        builder.finish()
148    }
149}
150
151impl fmt::Display for Error {
152    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153        match self.inner.kind {
154            Kind::Builder => f.write_str("builder error")?,
155            Kind::Request => f.write_str("error sending request")?,
156            Kind::Body => f.write_str("request or response body error")?,
157            Kind::Decode => f.write_str("error decoding response body")?,
158            Kind::Redirect => f.write_str("error following redirect")?,
159            Kind::Upgrade => f.write_str("error upgrading connection")?,
160            Kind::Status(ref code) => {
161                let prefix = if code.is_client_error() {
162                    "HTTP status client error"
163                } else {
164                    debug_assert!(code.is_server_error());
165                    "HTTP status server error"
166                };
167                write!(f, "{prefix} ({code})")?;
168            }
169        };
170
171        if let Some(url) = &self.inner.url {
172            write!(f, " for url ({url})")?;
173        }
174
175        Ok(())
176    }
177}
178
179impl StdError for Error {
180    fn source(&self) -> Option<&(dyn StdError + 'static)> {
181        self.inner.source.as_ref().map(|e| &**e as _)
182    }
183}
184
185#[derive(Debug)]
186#[allow(dead_code)]
187pub(crate) enum Kind {
188    Builder,
189    Request,
190    Redirect,
191    Status(StatusCode),
192    Body,
193    Decode,
194    Upgrade,
195}
196
197// constructors
198
199pub(crate) fn builder<E: Into<BoxError>>(e: E) -> Error {
200    Error::new(Kind::Builder, Some(e))
201}
202
203#[allow(dead_code)]
204pub(crate) fn body<E: Into<BoxError>>(e: E) -> Error {
205    Error::new(Kind::Body, Some(e))
206}
207
208pub(crate) fn decode<E: Into<BoxError>>(e: E) -> Error {
209    Error::new(Kind::Decode, Some(e))
210}
211
212#[allow(dead_code)]
213pub(crate) fn request<E: Into<BoxError>>(e: E) -> Error {
214    Error::new(Kind::Request, Some(e))
215}
216
217#[allow(dead_code)]
218pub(crate) fn redirect<E: Into<BoxError>>(e: E, url: Url) -> Error {
219    Error::new(Kind::Redirect, Some(e)).with_url(url)
220}
221
222pub(crate) fn status_code(url: Url, status: StatusCode) -> Error {
223    Error::new(Kind::Status(status), None::<Error>).with_url(url)
224}
225
226pub(crate) fn url_bad_scheme(url: Url) -> Error {
227    Error::new(Kind::Builder, Some(BadScheme)).with_url(url)
228}
229
230#[allow(dead_code)]
231pub(crate) fn url_invalid_uri(url: Url) -> Error {
232    Error::new(Kind::Builder, Some("Parsed Url is not a valid Uri")).with_url(url)
233}
234
235#[allow(dead_code)]
236pub(crate) fn upgrade<E: Into<BoxError>>(e: E) -> Error {
237    Error::new(Kind::Upgrade, Some(e))
238}
239
240// io::Error helpers
241
242#[allow(unused)]
243pub(crate) fn decode_io(e: io::Error) -> Error {
244    if e.get_ref().map(|r| r.is::<Error>()).unwrap_or(false) {
245        *e.into_inner()
246            .expect("io::Error::get_ref was Some(_)")
247            .downcast::<Error>()
248            .expect("StdError::is() was true")
249    } else {
250        decode(e)
251    }
252}
253
254// internal Error "sources"
255
256#[derive(Debug)]
257pub(crate) struct TimedOut;
258
259impl fmt::Display for TimedOut {
260    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
261        f.write_str("operation timed out")
262    }
263}
264
265impl StdError for TimedOut {}
266
267#[derive(Debug)]
268pub(crate) struct BadScheme;
269
270impl fmt::Display for BadScheme {
271    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
272        f.write_str("URL scheme is not allowed")
273    }
274}
275
276impl StdError for BadScheme {}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    fn assert_send<T: Send>() {}
283    fn assert_sync<T: Sync>() {}
284
285    #[test]
286    fn test_source_chain() {
287        let root = Error::new(Kind::Request, None::<Error>);
288        assert!(root.source().is_none());
289
290        let link = super::body(root);
291        assert!(link.source().is_some());
292        assert_send::<Error>();
293        assert_sync::<Error>();
294    }
295
296    #[test]
297    fn mem_size_of() {
298        use std::mem::size_of;
299        assert_eq!(size_of::<Error>(), size_of::<usize>());
300    }
301
302    #[test]
303    fn roundtrip_io_error() {
304        let orig = super::request("orig");
305        // Convert crate::Error into an io::Error...
306        let io = orig.into_io();
307        // Convert that io::Error back into a crate::Error...
308        let err = super::decode_io(io);
309        // It should have pulled out the original, not nested it...
310        match err.inner.kind {
311            Kind::Request => (),
312            _ => panic!("{err:?}"),
313        }
314    }
315
316    #[test]
317    fn from_unknown_io_error() {
318        let orig = io::Error::other("orly");
319        let err = super::decode_io(orig);
320        match err.inner.kind {
321            Kind::Decode => (),
322            _ => panic!("{err:?}"),
323        }
324    }
325
326    #[test]
327    fn is_timeout() {
328        let err = super::request(super::TimedOut);
329        assert!(err.is_timeout());
330
331        let io = io::Error::other(err);
332        let nested = super::request(io);
333        assert!(nested.is_timeout());
334    }
335}