| 1 | use url::{ParseError, Url}; |
| 2 | |
| 3 | use std::error; |
| 4 | use std::fmt::{self, Display}; |
| 5 | use std::io; |
| 6 | |
| 7 | use crate::Response; |
| 8 | |
| 9 | /// An error that may occur when processing a [Request](crate::Request). |
| 10 | /// |
| 11 | /// This can represent connection-level errors (e.g. connection refused), |
| 12 | /// protocol-level errors (malformed response), or status code errors |
| 13 | /// (e.g. 404 Not Found). Status code errors are represented by the |
| 14 | /// [Status](Error::Status) enum variant, while connection-level and |
| 15 | /// protocol-level errors are represented by the [Transport](Error::Transport) |
| 16 | /// enum variant. You can use a match statement to extract a Response |
| 17 | /// from a `Status` error. For instance, you may want to read the full |
| 18 | /// body of a response because you expect it to contain a useful error |
| 19 | /// message. Or you may want to handle certain error code responses |
| 20 | /// differently. |
| 21 | /// |
| 22 | /// # Examples |
| 23 | /// |
| 24 | /// Example of matching out all unexpected server status codes. |
| 25 | /// |
| 26 | /// ```no_run |
| 27 | /// use ureq::Error; |
| 28 | /// |
| 29 | /// match ureq::get("http://mypage.example.com/" ).call() { |
| 30 | /// Ok(response) => { /* it worked */}, |
| 31 | /// Err(Error::Status(code, response)) => { |
| 32 | /// /* the server returned an unexpected status |
| 33 | /// code (such as 400, 500 etc) */ |
| 34 | /// } |
| 35 | /// Err(_) => { /* some kind of io/transport error */ } |
| 36 | /// } |
| 37 | /// ``` |
| 38 | /// |
| 39 | /// An example of a function that handles HTTP 429 and 500 errors differently |
| 40 | /// than other errors. They get retried after a suitable delay, up to 4 times. |
| 41 | /// |
| 42 | /// ``` |
| 43 | /// use std::{result::Result, time::Duration, thread}; |
| 44 | /// use ureq::{Response, Error, Error::Status}; |
| 45 | /// # fn main(){ ureq::is_test(true); get_response( "http://httpbin.org/status/500" ); } |
| 46 | /// |
| 47 | /// fn get_response(url: &str) -> Result<Response, Error> { |
| 48 | /// for _ in 1..4 { |
| 49 | /// match ureq::get(url).call() { |
| 50 | /// Err(Status(503, r)) | Err(Status(429, r)) => { |
| 51 | /// let retry: Option<u64> = r.header("retry-after" ) |
| 52 | /// .and_then(|h| h.parse().ok()); |
| 53 | /// let retry = retry.unwrap_or(5); |
| 54 | /// eprintln!("{} for {}, retry in {}" , r.status(), r.get_url(), retry); |
| 55 | /// thread::sleep(Duration::from_secs(retry)); |
| 56 | /// } |
| 57 | /// result => return result, |
| 58 | /// }; |
| 59 | /// } |
| 60 | /// // Ran out of retries; try one last time and return whatever result we get. |
| 61 | /// ureq::get(url).call() |
| 62 | /// } |
| 63 | /// ``` |
| 64 | /// |
| 65 | /// If you'd like to treat all status code errors as normal, successful responses, |
| 66 | /// you can use [OrAnyStatus::or_any_status] like this: |
| 67 | /// |
| 68 | /// ``` |
| 69 | /// use ureq::Error::Status; |
| 70 | /// # fn main() -> std::result::Result<(), ureq::Transport> { |
| 71 | /// # ureq::is_test(true); |
| 72 | /// use ureq::OrAnyStatus; |
| 73 | /// |
| 74 | /// let resp = ureq::get("http://example.com/" ) |
| 75 | /// .call() |
| 76 | /// .or_any_status()?; |
| 77 | /// # Ok(()) |
| 78 | /// # } |
| 79 | /// ``` |
| 80 | #[derive (Debug)] |
| 81 | pub enum Error { |
| 82 | /// A response was successfully received but had status code >= 400. |
| 83 | /// Values are (status_code, Response). |
| 84 | Status(u16, Response), |
| 85 | /// There was an error making the request or receiving the response. |
| 86 | Transport(Transport), |
| 87 | } |
| 88 | |
| 89 | impl Error { |
| 90 | /// Optionally turn this error into an underlying `Transport`. |
| 91 | /// |
| 92 | /// `None` if the underlying error is `Error::Status`. |
| 93 | pub fn into_transport(self) -> Option<Transport> { |
| 94 | match self { |
| 95 | Error::Status(_, _) => None, |
| 96 | Error::Transport(t: Transport) => Some(t), |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /// Optionally turn this error into an underlying `Response`. |
| 101 | /// |
| 102 | /// `None` if the underlying error is `Error::Transport`. |
| 103 | pub fn into_response(self) -> Option<Response> { |
| 104 | match self { |
| 105 | Error::Status(_, r: Response) => Some(r), |
| 106 | Error::Transport(_) => None, |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | /// Error that is not a status code error. For instance, DNS name not found, |
| 112 | /// connection refused, or malformed response. |
| 113 | /// |
| 114 | /// * [`Transport::kind()`] provides a classification (same as for [`Error::kind`]). |
| 115 | /// * [`Transport::message()`] might vary for the same classification to give more context. |
| 116 | /// * [`Transport::source()`](std::error::Error::source) holds the underlying error with even more details. |
| 117 | /// |
| 118 | /// ``` |
| 119 | /// # fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 120 | /// use ureq::ErrorKind; |
| 121 | /// use std::error::Error; |
| 122 | /// use url::ParseError; |
| 123 | /// |
| 124 | /// let result = ureq::get("broken/url" ).call(); |
| 125 | /// let error = result.unwrap_err().into_transport().unwrap(); |
| 126 | /// |
| 127 | /// // the display trait is a combo of the underlying classifications |
| 128 | /// assert_eq!(error.to_string(), |
| 129 | /// "Bad URL: failed to parse URL: RelativeUrlWithoutBase: relative URL without a base" ); |
| 130 | /// |
| 131 | /// // classification |
| 132 | /// assert_eq!(error.kind(), ErrorKind::InvalidUrl); |
| 133 | /// assert_eq!(error.kind().to_string(), "Bad URL" ); |
| 134 | /// |
| 135 | /// // higher level message |
| 136 | /// assert_eq!(error.message(), Some("failed to parse URL: RelativeUrlWithoutBase" )); |
| 137 | /// |
| 138 | /// // boxed underlying error |
| 139 | /// let source = error.source().unwrap(); |
| 140 | /// // downcast to original error |
| 141 | /// let downcast: &ParseError = source.downcast_ref().unwrap(); |
| 142 | /// |
| 143 | /// assert_eq!(downcast.to_string(), "relative URL without a base" ); |
| 144 | /// # Ok(()) |
| 145 | /// # } |
| 146 | /// ``` |
| 147 | #[derive (Debug)] |
| 148 | pub struct Transport { |
| 149 | kind: ErrorKind, |
| 150 | message: Option<String>, |
| 151 | url: Option<Url>, |
| 152 | source: Option<Box<dyn error::Error + Send + Sync + 'static>>, |
| 153 | } |
| 154 | |
| 155 | impl Transport { |
| 156 | /// The type of error that happened while processing the request. |
| 157 | pub fn kind(&self) -> ErrorKind { |
| 158 | self.kind |
| 159 | } |
| 160 | |
| 161 | /// Higher level error details, if there are any. |
| 162 | pub fn message(&self) -> Option<&str> { |
| 163 | self.message.as_deref() |
| 164 | } |
| 165 | |
| 166 | /// The url that failed. This can be interesting in cases of redirect where |
| 167 | /// the original url worked, but a later redirected to url fails. |
| 168 | pub fn url(&self) -> Option<&Url> { |
| 169 | self.url.as_ref() |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | /// Extension to [`Result<Response, Error>`] for handling all status codes as [`Response`]. |
| 174 | pub trait OrAnyStatus { |
| 175 | /// Ergonomic helper for handling all status codes as [`Response`]. |
| 176 | /// |
| 177 | /// By default, ureq returns non-2xx responses as [`Error::Status`]. This |
| 178 | /// helper is for handling all responses as [`Response`], regardless |
| 179 | /// of status code. |
| 180 | /// |
| 181 | /// ``` |
| 182 | /// # ureq::is_test(true); |
| 183 | /// # fn main() -> Result<(), ureq::Transport> { |
| 184 | /// // Bring trait into context. |
| 185 | /// use ureq::OrAnyStatus; |
| 186 | /// |
| 187 | /// let response = ureq::get("http://httpbin.org/status/500" ) |
| 188 | /// .call() |
| 189 | /// // Transport errors, such as DNS or connectivity problems |
| 190 | /// // must still be dealt with as `Err`. |
| 191 | /// .or_any_status()?; |
| 192 | /// |
| 193 | /// assert_eq!(response.status(), 500); |
| 194 | /// # Ok(()) |
| 195 | /// # } |
| 196 | /// ``` |
| 197 | fn or_any_status(self) -> Result<Response, Transport>; |
| 198 | } |
| 199 | |
| 200 | impl OrAnyStatus for Result<Response, Error> { |
| 201 | fn or_any_status(self) -> Result<Response, Transport> { |
| 202 | match self { |
| 203 | Ok(response: Response) => Ok(response), |
| 204 | Err(Error::Status(_, response: Response)) => Ok(response), |
| 205 | Err(Error::Transport(transport: Transport)) => Err(transport), |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | impl Display for Error { |
| 211 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 212 | match self { |
| 213 | Error::Status(status: &u16, response: &Response) => { |
| 214 | write!(f, " {}: status code {}" , response.get_url(), status)?; |
| 215 | if let Some(original: &Url) = response.history.first() { |
| 216 | write!(f, " (redirected from {})" , original)?; |
| 217 | } |
| 218 | } |
| 219 | Error::Transport(err: &Transport) => { |
| 220 | write!(f, " {}" , err)?; |
| 221 | } |
| 222 | } |
| 223 | Ok(()) |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | impl Display for Transport { |
| 228 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 229 | if let Some(url: &Url) = &self.url { |
| 230 | write!(f, " {}: " , url)?; |
| 231 | } |
| 232 | write!(f, " {}" , self.kind)?; |
| 233 | if let Some(message: &String) = &self.message { |
| 234 | write!(f, ": {}" , message)?; |
| 235 | } |
| 236 | if let Some(source: &Box) = &self.source { |
| 237 | write!(f, ": {}" , source)?; |
| 238 | } |
| 239 | Ok(()) |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | impl error::Error for Error { |
| 244 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { |
| 245 | match &self { |
| 246 | Error::Transport(Transport { |
| 247 | source: Some(s: &Box), .. |
| 248 | }) => Some(s.as_ref()), |
| 249 | _ => None, |
| 250 | } |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | impl error::Error for Transport { |
| 255 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { |
| 256 | self.source |
| 257 | .as_ref() |
| 258 | .map(|s: &Box| s.as_ref() as &(dyn error::Error + 'static)) |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | impl Error { |
| 263 | pub(crate) fn new(kind: ErrorKind, message: Option<String>) -> Self { |
| 264 | Error::Transport(Transport { |
| 265 | kind, |
| 266 | message, |
| 267 | url: None, |
| 268 | source: None, |
| 269 | }) |
| 270 | } |
| 271 | |
| 272 | pub(crate) fn url(self, url: Url) -> Self { |
| 273 | if let Error::Transport(mut e) = self { |
| 274 | e.url = Some(url); |
| 275 | Error::Transport(e) |
| 276 | } else { |
| 277 | self |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | pub(crate) fn src(self, e: impl error::Error + Send + Sync + 'static) -> Self { |
| 282 | if let Error::Transport(mut oe) = self { |
| 283 | oe.source = Some(Box::new(e)); |
| 284 | Error::Transport(oe) |
| 285 | } else { |
| 286 | self |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | /// The type of this error. |
| 291 | /// |
| 292 | /// ``` |
| 293 | /// # ureq::is_test(true); |
| 294 | /// let err = ureq::get("http://httpbin.org/status/500" ) |
| 295 | /// .call().unwrap_err(); |
| 296 | /// assert_eq!(err.kind(), ureq::ErrorKind::HTTP); |
| 297 | /// ``` |
| 298 | pub fn kind(&self) -> ErrorKind { |
| 299 | match self { |
| 300 | Error::Status(_, _) => ErrorKind::HTTP, |
| 301 | Error::Transport(Transport { kind: k, .. }) => *k, |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | /// Return true iff the error was due to a connection closing. |
| 306 | pub(crate) fn connection_closed(&self) -> bool { |
| 307 | if self.kind() != ErrorKind::Io { |
| 308 | return false; |
| 309 | } |
| 310 | let other_err = match self { |
| 311 | Error::Status(_, _) => return false, |
| 312 | Error::Transport(e) => e, |
| 313 | }; |
| 314 | let source = match other_err.source.as_ref() { |
| 315 | Some(e) => e, |
| 316 | None => return false, |
| 317 | }; |
| 318 | let ioe: &io::Error = match source.downcast_ref() { |
| 319 | Some(e) => e, |
| 320 | None => return false, |
| 321 | }; |
| 322 | match ioe.kind() { |
| 323 | io::ErrorKind::ConnectionAborted => true, |
| 324 | io::ErrorKind::ConnectionReset => true, |
| 325 | _ => false, |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | /// One of the types of error the can occur when processing a Request. |
| 331 | #[derive (Debug, PartialEq, Eq, Clone, Copy)] |
| 332 | pub enum ErrorKind { |
| 333 | /// The url could not be understood. |
| 334 | InvalidUrl, |
| 335 | /// The url scheme could not be understood. |
| 336 | UnknownScheme, |
| 337 | /// DNS lookup failed. |
| 338 | Dns, |
| 339 | /// Insecure request attempted with https only set |
| 340 | InsecureRequestHttpsOnly, |
| 341 | /// Connection to server failed. |
| 342 | ConnectionFailed, |
| 343 | /// Too many redirects. |
| 344 | TooManyRedirects, |
| 345 | /// A status line we don't understand `HTTP/1.1 200 OK`. |
| 346 | BadStatus, |
| 347 | /// A header line that couldn't be parsed. |
| 348 | BadHeader, |
| 349 | /// Some unspecified `std::io::Error`. |
| 350 | Io, |
| 351 | /// Proxy information was not properly formatted |
| 352 | InvalidProxyUrl, |
| 353 | /// Proxy could not connect |
| 354 | ProxyConnect, |
| 355 | /// Incorrect credentials for proxy |
| 356 | ProxyUnauthorized, |
| 357 | /// HTTP status code indicating an error (e.g. 4xx, 5xx) |
| 358 | /// Read the inner response body for details and to return |
| 359 | /// the connection to the pool. |
| 360 | HTTP, |
| 361 | } |
| 362 | |
| 363 | impl ErrorKind { |
| 364 | #[allow (clippy::wrong_self_convention)] |
| 365 | #[allow (clippy::new_ret_no_self)] |
| 366 | pub(crate) fn new(self) -> Error { |
| 367 | Error::new(self, message:None) |
| 368 | } |
| 369 | |
| 370 | pub(crate) fn msg(self, s: impl Into<String>) -> Error { |
| 371 | Error::new(self, message:Some(s.into())) |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | impl From<Response> for Error { |
| 376 | fn from(resp: Response) -> Error { |
| 377 | Error::Status(resp.status(), resp) |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | impl From<io::Error> for Error { |
| 382 | fn from(err: io::Error) -> Error { |
| 383 | ErrorKind::Io.new().src(err) |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | impl From<Transport> for Error { |
| 388 | fn from(err: Transport) -> Error { |
| 389 | Error::Transport(err) |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | impl From<ParseError> for Error { |
| 394 | fn from(err: ParseError) -> Self { |
| 395 | ErrorKindError::InvalidUrl |
| 396 | .msg(format!("failed to parse URL: {:?}" , err)) |
| 397 | .src(err) |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | impl fmt::Display for ErrorKind { |
| 402 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 403 | match self { |
| 404 | ErrorKind::InvalidUrl => write!(f, "Bad URL" ), |
| 405 | ErrorKind::UnknownScheme => write!(f, "Unknown Scheme" ), |
| 406 | ErrorKind::Dns => write!(f, "Dns Failed" ), |
| 407 | ErrorKind::InsecureRequestHttpsOnly => { |
| 408 | write!(f, "Insecure request attempted with https_only set" ) |
| 409 | } |
| 410 | ErrorKind::ConnectionFailed => write!(f, "Connection Failed" ), |
| 411 | ErrorKind::TooManyRedirects => write!(f, "Too Many Redirects" ), |
| 412 | ErrorKind::BadStatus => write!(f, "Bad Status" ), |
| 413 | ErrorKind::BadHeader => write!(f, "Bad Header" ), |
| 414 | ErrorKind::Io => write!(f, "Network Error" ), |
| 415 | ErrorKind::InvalidProxyUrl => write!(f, "Malformed proxy" ), |
| 416 | ErrorKind::ProxyConnect => write!(f, "Proxy failed to connect" ), |
| 417 | ErrorKind::ProxyUnauthorized => write!(f, "Provided proxy credentials are incorrect" ), |
| 418 | ErrorKind::HTTP => write!(f, "HTTP status error" ), |
| 419 | } |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | #[cfg (test)] |
| 424 | mod tests { |
| 425 | use super::*; |
| 426 | |
| 427 | #[test ] |
| 428 | fn status_code_error() { |
| 429 | let mut response = Response::new(404, "NotFound" , "" ).unwrap(); |
| 430 | response.set_url("http://example.org/" .parse().unwrap()); |
| 431 | let err = Error::Status(response.status(), response); |
| 432 | |
| 433 | assert_eq!(err.to_string(), "http://example.org/: status code 404" ); |
| 434 | } |
| 435 | |
| 436 | #[test ] |
| 437 | fn status_code_error_redirect() { |
| 438 | use crate::{get, test}; |
| 439 | |
| 440 | test::set_handler("/redirect_a" , |unit| { |
| 441 | assert_eq!(unit.method, "GET" ); |
| 442 | test::make_response( |
| 443 | 302, |
| 444 | "Go here" , |
| 445 | vec!["Location: test://example.edu/redirect_b" ], |
| 446 | vec![], |
| 447 | ) |
| 448 | }); |
| 449 | test::set_handler("/redirect_b" , |unit| { |
| 450 | assert_eq!(unit.method, "GET" ); |
| 451 | test::make_response( |
| 452 | 302, |
| 453 | "Go here" , |
| 454 | vec!["Location: http://example.com/status/500" ], |
| 455 | vec![], |
| 456 | ) |
| 457 | }); |
| 458 | |
| 459 | let err = get("test://example.org/redirect_a" ).call().unwrap_err(); |
| 460 | assert_eq!(err.kind(), ErrorKind::HTTP, "{:?}" , err); |
| 461 | assert_eq!( |
| 462 | err.to_string(), |
| 463 | "http://example.com/status/500: status code 500 (redirected from test://example.org/redirect_a)" |
| 464 | ); |
| 465 | } |
| 466 | |
| 467 | #[test ] |
| 468 | fn io_error() { |
| 469 | let ioe = io::Error::new(io::ErrorKind::TimedOut, "too slow" ); |
| 470 | let mut err = Error::new(ErrorKind::Io, Some("oops" .to_string())).src(ioe); |
| 471 | |
| 472 | err = err.url("http://example.com/" .parse().unwrap()); |
| 473 | assert_eq!( |
| 474 | err.to_string(), |
| 475 | "http://example.com/: Network Error: oops: too slow" |
| 476 | ); |
| 477 | } |
| 478 | |
| 479 | #[test ] |
| 480 | fn connection_closed() { |
| 481 | let ioe = io::Error::new(io::ErrorKind::ConnectionReset, "connection reset" ); |
| 482 | let err = ErrorKind::Io.new().src(ioe); |
| 483 | assert!(err.connection_closed()); |
| 484 | |
| 485 | let ioe = io::Error::new(io::ErrorKind::ConnectionAborted, "connection aborted" ); |
| 486 | let err = ErrorKind::Io.new().src(ioe); |
| 487 | assert!(err.connection_closed()); |
| 488 | } |
| 489 | |
| 490 | #[test ] |
| 491 | fn error_implements_send_and_sync() { |
| 492 | let _error: Box<dyn Send> = Box::new(Error::new(ErrorKind::Io, None)); |
| 493 | let _error: Box<dyn Sync> = Box::new(Error::new(ErrorKind::Io, None)); |
| 494 | } |
| 495 | |
| 496 | #[test ] |
| 497 | fn ensure_error_size() { |
| 498 | // This is platform dependent, so we can't be too strict or precise. |
| 499 | let size = std::mem::size_of::<Error>(); |
| 500 | println!("Error size: {}" , size); |
| 501 | assert!(size < 500); // 344 on Macbook M1 |
| 502 | } |
| 503 | } |
| 504 | |