367 lines
9.6 KiB
Rust
367 lines
9.6 KiB
Rust
#![cfg_attr(target_arch = "wasm32", allow(unused))]
|
|
use std::error::Error as StdError;
|
|
use std::fmt;
|
|
use std::io;
|
|
|
|
use crate::{StatusCode, Url};
|
|
|
|
/// A `Result` alias where the `Err` case is `reqwest::Error`.
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
/// The Errors that may occur when processing a `Request`.
|
|
///
|
|
/// Note: Errors may include the full URL used to make the `Request`. If the URL
|
|
/// contains sensitive information (e.g. an API key as a query parameter), be
|
|
/// sure to remove it ([`without_url`](Error::without_url))
|
|
pub struct Error {
|
|
inner: Box<Inner>,
|
|
}
|
|
|
|
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
|
|
|
|
struct Inner {
|
|
kind: Kind,
|
|
source: Option<BoxError>,
|
|
url: Option<Url>,
|
|
}
|
|
|
|
impl Error {
|
|
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
|
where
|
|
E: Into<BoxError>,
|
|
{
|
|
Error {
|
|
inner: Box::new(Inner {
|
|
kind,
|
|
source: source.map(Into::into),
|
|
url: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Returns a possible URL related to this error.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # async fn run() {
|
|
/// // displays last stop of a redirect loop
|
|
/// let response = reqwest::get("http://site.with.redirect.loop").await;
|
|
/// if let Err(e) = response {
|
|
/// if e.is_redirect() {
|
|
/// if let Some(final_stop) = e.url() {
|
|
/// println!("redirect loop at {}", final_stop);
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// # }
|
|
/// ```
|
|
pub fn url(&self) -> Option<&Url> {
|
|
self.inner.url.as_ref()
|
|
}
|
|
|
|
/// Returns a mutable reference to the URL related to this error
|
|
///
|
|
/// This is useful if you need to remove sensitive information from the URL
|
|
/// (e.g. an API key in the query), but do not want to remove the URL
|
|
/// entirely.
|
|
pub fn url_mut(&mut self) -> Option<&mut Url> {
|
|
self.inner.url.as_mut()
|
|
}
|
|
|
|
/// Add a url related to this error (overwriting any existing)
|
|
pub fn with_url(mut self, url: Url) -> Self {
|
|
self.inner.url = Some(url);
|
|
self
|
|
}
|
|
|
|
/// Strip the related url from this error (if, for example, it contains
|
|
/// sensitive information)
|
|
pub fn without_url(mut self) -> Self {
|
|
self.inner.url = None;
|
|
self
|
|
}
|
|
|
|
/// Returns true if the error is from a type Builder.
|
|
pub fn is_builder(&self) -> bool {
|
|
matches!(self.inner.kind, Kind::Builder)
|
|
}
|
|
|
|
/// Returns true if the error is from a `RedirectPolicy`.
|
|
pub fn is_redirect(&self) -> bool {
|
|
matches!(self.inner.kind, Kind::Redirect)
|
|
}
|
|
|
|
/// Returns true if the error is from `Response::error_for_status`.
|
|
pub fn is_status(&self) -> bool {
|
|
matches!(self.inner.kind, Kind::Status(_))
|
|
}
|
|
|
|
/// Returns true if the error is related to a timeout.
|
|
pub fn is_timeout(&self) -> bool {
|
|
let mut source = self.source();
|
|
|
|
while let Some(err) = source {
|
|
if err.is::<TimedOut>() {
|
|
return true;
|
|
}
|
|
source = err.source();
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Returns true if the error is related to the request
|
|
pub fn is_request(&self) -> bool {
|
|
matches!(self.inner.kind, Kind::Request)
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
/// Returns true if the error is related to connect
|
|
pub fn is_connect(&self) -> bool {
|
|
let mut source = self.source();
|
|
|
|
while let Some(err) = source {
|
|
if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
|
|
if hyper_err.is_connect() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
source = err.source();
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Returns true if the error is related to the request or response body
|
|
pub fn is_body(&self) -> bool {
|
|
matches!(self.inner.kind, Kind::Body)
|
|
}
|
|
|
|
/// Returns true if the error is related to decoding the response's body
|
|
pub fn is_decode(&self) -> bool {
|
|
matches!(self.inner.kind, Kind::Decode)
|
|
}
|
|
|
|
/// Returns the status code, if the error was generated from a response.
|
|
pub fn status(&self) -> Option<StatusCode> {
|
|
match self.inner.kind {
|
|
Kind::Status(code) => Some(code),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// private
|
|
|
|
#[allow(unused)]
|
|
pub(crate) fn into_io(self) -> io::Error {
|
|
io::Error::new(io::ErrorKind::Other, self)
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Error {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut builder = f.debug_struct("reqwest::Error");
|
|
|
|
builder.field("kind", &self.inner.kind);
|
|
|
|
if let Some(ref url) = self.inner.url {
|
|
builder.field("url", url);
|
|
}
|
|
if let Some(ref source) = self.inner.source {
|
|
builder.field("source", source);
|
|
}
|
|
|
|
builder.finish()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Error {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self.inner.kind {
|
|
Kind::Builder => f.write_str("builder error")?,
|
|
Kind::Request => f.write_str("error sending request")?,
|
|
Kind::Body => f.write_str("request or response body error")?,
|
|
Kind::Decode => f.write_str("error decoding response body")?,
|
|
Kind::Redirect => f.write_str("error following redirect")?,
|
|
Kind::Status(ref code) => {
|
|
let prefix = if code.is_client_error() {
|
|
"HTTP status client error"
|
|
} else {
|
|
debug_assert!(code.is_server_error());
|
|
"HTTP status server error"
|
|
};
|
|
write!(f, "{} ({})", prefix, code)?;
|
|
}
|
|
};
|
|
|
|
if let Some(url) = &self.inner.url {
|
|
write!(f, " for url ({})", url.as_str())?;
|
|
}
|
|
|
|
if let Some(e) = &self.inner.source {
|
|
write!(f, ": {}", e)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl StdError for Error {
|
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
|
self.inner.source.as_ref().map(|e| &**e as _)
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
impl From<crate::error::Error> for wasm_bindgen::JsValue {
|
|
fn from(err: Error) -> wasm_bindgen::JsValue {
|
|
js_sys::Error::from(err).into()
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
impl From<crate::error::Error> for js_sys::Error {
|
|
fn from(err: Error) -> js_sys::Error {
|
|
js_sys::Error::new(&format!("{}", err))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum Kind {
|
|
Builder,
|
|
Request,
|
|
Redirect,
|
|
Status(StatusCode),
|
|
Body,
|
|
Decode,
|
|
}
|
|
|
|
// constructors
|
|
|
|
pub(crate) fn builder<E: Into<BoxError>>(e: E) -> Error {
|
|
Error::new(Kind::Builder, Some(e))
|
|
}
|
|
|
|
pub(crate) fn body<E: Into<BoxError>>(e: E) -> Error {
|
|
Error::new(Kind::Body, Some(e))
|
|
}
|
|
|
|
pub(crate) fn decode<E: Into<BoxError>>(e: E) -> Error {
|
|
Error::new(Kind::Decode, Some(e))
|
|
}
|
|
|
|
pub(crate) fn request<E: Into<BoxError>>(e: E) -> Error {
|
|
Error::new(Kind::Request, Some(e))
|
|
}
|
|
|
|
pub(crate) fn redirect<E: Into<BoxError>>(e: E, url: Url) -> Error {
|
|
Error::new(Kind::Redirect, Some(e)).with_url(url)
|
|
}
|
|
|
|
pub(crate) fn status_code(url: Url, status: StatusCode) -> Error {
|
|
Error::new(Kind::Status(status), None::<Error>).with_url(url)
|
|
}
|
|
|
|
pub(crate) fn url_bad_scheme(url: Url) -> Error {
|
|
Error::new(Kind::Builder, Some("URL scheme is not allowed")).with_url(url)
|
|
}
|
|
|
|
if_wasm! {
|
|
pub(crate) fn wasm(js_val: wasm_bindgen::JsValue) -> BoxError {
|
|
format!("{:?}", js_val).into()
|
|
}
|
|
}
|
|
|
|
// io::Error helpers
|
|
|
|
#[allow(unused)]
|
|
pub(crate) fn into_io(e: Error) -> io::Error {
|
|
e.into_io()
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub(crate) fn decode_io(e: io::Error) -> Error {
|
|
if e.get_ref().map(|r| r.is::<Error>()).unwrap_or(false) {
|
|
*e.into_inner()
|
|
.expect("io::Error::get_ref was Some(_)")
|
|
.downcast::<Error>()
|
|
.expect("StdError::is() was true")
|
|
} else {
|
|
decode(e)
|
|
}
|
|
}
|
|
|
|
// internal Error "sources"
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct TimedOut;
|
|
|
|
impl fmt::Display for TimedOut {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.write_str("operation timed out")
|
|
}
|
|
}
|
|
|
|
impl StdError for TimedOut {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn assert_send<T: Send>() {}
|
|
fn assert_sync<T: Sync>() {}
|
|
|
|
#[test]
|
|
fn test_source_chain() {
|
|
let root = Error::new(Kind::Request, None::<Error>);
|
|
assert!(root.source().is_none());
|
|
|
|
let link = super::body(root);
|
|
assert!(link.source().is_some());
|
|
assert_send::<Error>();
|
|
assert_sync::<Error>();
|
|
}
|
|
|
|
#[test]
|
|
fn mem_size_of() {
|
|
use std::mem::size_of;
|
|
assert_eq!(size_of::<Error>(), size_of::<usize>());
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_io_error() {
|
|
let orig = super::request("orig");
|
|
// Convert reqwest::Error into an io::Error...
|
|
let io = orig.into_io();
|
|
// Convert that io::Error back into a reqwest::Error...
|
|
let err = super::decode_io(io);
|
|
// It should have pulled out the original, not nested it...
|
|
match err.inner.kind {
|
|
Kind::Request => (),
|
|
_ => panic!("{:?}", err),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn from_unknown_io_error() {
|
|
let orig = io::Error::new(io::ErrorKind::Other, "orly");
|
|
let err = super::decode_io(orig);
|
|
match err.inner.kind {
|
|
Kind::Decode => (),
|
|
_ => panic!("{:?}", err),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn is_timeout() {
|
|
let err = super::request(super::TimedOut);
|
|
assert!(err.is_timeout());
|
|
|
|
let io = io::Error::new(io::ErrorKind::Other, err);
|
|
let nested = super::request(io);
|
|
assert!(nested.is_timeout());
|
|
}
|
|
}
|