This is an automated email from the ASF dual-hosted git repository.

tustvold pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new f845d6e7b Handle incomplete HTTP redirects missing LOCATION (#2795) 
(#2796)
f845d6e7b is described below

commit f845d6e7b82dbcdfac31266ed91111d2c5a7eb69
Author: Raphael Taylor-Davies <[email protected]>
AuthorDate: Thu Sep 29 07:33:31 2022 +0100

    Handle incomplete HTTP redirects missing LOCATION (#2795) (#2796)
---
 object_store/src/client/retry.rs | 119 ++++++++++++++++++++++++++++++---------
 1 file changed, 92 insertions(+), 27 deletions(-)

diff --git a/object_store/src/client/retry.rs b/object_store/src/client/retry.rs
index d66628aec..cee86b344 100644
--- a/object_store/src/client/retry.rs
+++ b/object_store/src/client/retry.rs
@@ -20,49 +20,62 @@
 use crate::client::backoff::{Backoff, BackoffConfig};
 use futures::future::BoxFuture;
 use futures::FutureExt;
+use reqwest::header::LOCATION;
 use reqwest::{Response, StatusCode};
-use snafu::Snafu;
 use std::time::{Duration, Instant};
 use tracing::info;
 
 /// Retry request error
-#[derive(Debug, Snafu)]
-#[snafu(display(
-    "response error \"{}\", after {} retries: {}",
-    message,
-    retries,
-    source
-))]
+#[derive(Debug)]
 pub struct Error {
     retries: usize,
     message: String,
-    source: reqwest::Error,
+    source: Option<reqwest::Error>,
+}
+
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "response error \"{}\", after {} retries",
+            self.message, self.retries
+        )?;
+        if let Some(source) = &self.source {
+            write!(f, ": {}", source)?;
+        }
+        Ok(())
+    }
+}
+
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        self.source.as_ref().map(|e| e as _)
+    }
 }
 
 impl Error {
     /// Returns the status code associated with this error if any
     pub fn status(&self) -> Option<StatusCode> {
-        self.source.status()
+        self.source.as_ref().and_then(|e| e.status())
     }
 }
 
 impl From<Error> for std::io::Error {
     fn from(err: Error) -> Self {
         use std::io::ErrorKind;
-        if err.source.is_builder() || err.source.is_request() {
-            Self::new(ErrorKind::InvalidInput, err)
-        } else if let Some(s) = err.source.status() {
-            match s {
-                StatusCode::NOT_FOUND => Self::new(ErrorKind::NotFound, err),
-                StatusCode::BAD_REQUEST => Self::new(ErrorKind::InvalidInput, 
err),
-                _ => Self::new(ErrorKind::Other, err),
+        match (&err.source, err.status()) {
+            (Some(source), _) if source.is_builder() || source.is_request() => 
{
+                Self::new(ErrorKind::InvalidInput, err)
+            }
+            (_, Some(StatusCode::NOT_FOUND)) => Self::new(ErrorKind::NotFound, 
err),
+            (_, Some(StatusCode::BAD_REQUEST)) => 
Self::new(ErrorKind::InvalidInput, err),
+            (Some(source), None) if source.is_timeout() => {
+                Self::new(ErrorKind::TimedOut, err)
+            }
+            (Some(source), None) if source.is_connect() => {
+                Self::new(ErrorKind::NotConnected, err)
             }
-        } else if err.source.is_timeout() {
-            Self::new(ErrorKind::TimedOut, err)
-        } else if err.source.is_connect() {
-            Self::new(ErrorKind::NotConnected, err)
-        } else {
-            Self::new(ErrorKind::Other, err)
+            _ => Self::new(ErrorKind::Other, err),
         }
     }
 }
@@ -131,7 +144,21 @@ impl RetryExt for reqwest::RequestBuilder {
                 let s = self.try_clone().expect("request body must be 
cloneable");
                 match s.send().await {
                     Ok(r) => match r.error_for_status_ref() {
-                        Ok(_) => return Ok(r),
+                        Ok(_) if r.status().is_success() => return Ok(r),
+                        Ok(r) => {
+                            let is_bare_redirect = r.status().is_redirection() 
&& !r.headers().contains_key(LOCATION);
+                            let message = match is_bare_redirect {
+                                true => "Received redirect without LOCATION, 
this normally indicates an incorrectly configured region".to_string(),
+                                // Not actually sure if this is reachable, but 
here for completeness
+                                false => format!("request unsuccessful: {}", 
r.status()),
+                            };
+
+                            return Err(Error{
+                                message,
+                                retries,
+                                source: None,
+                            })
+                        }
                         Err(e) => {
                             let status = r.status();
 
@@ -152,7 +179,7 @@ impl RetryExt for reqwest::RequestBuilder {
                                 return Err(Error{
                                     message,
                                     retries,
-                                    source: e,
+                                    source: Some(e),
                                 })
 
                             }
@@ -168,7 +195,7 @@ impl RetryExt for reqwest::RequestBuilder {
                         return Err(Error{
                             retries,
                             message: "request error".to_string(),
-                            source: e
+                            source: Some(e)
                         })
                     }
                 }
@@ -253,7 +280,7 @@ mod tests {
         let r = do_request().await.unwrap();
         assert_eq!(r.status(), StatusCode::NO_CONTENT);
 
-        // Follows redirects
+        // Follows 402 redirects
         mock.push(
             Response::builder()
                 .status(StatusCode::FOUND)
@@ -266,6 +293,44 @@ mod tests {
         assert_eq!(r.status(), StatusCode::OK);
         assert_eq!(r.url().path(), "/foo");
 
+        // Follows 401 redirects
+        mock.push(
+            Response::builder()
+                .status(StatusCode::FOUND)
+                .header(LOCATION, "/bar")
+                .body(Body::empty())
+                .unwrap(),
+        );
+
+        let r = do_request().await.unwrap();
+        assert_eq!(r.status(), StatusCode::OK);
+        assert_eq!(r.url().path(), "/bar");
+
+        // Handles redirect loop
+        for _ in 0..10 {
+            mock.push(
+                Response::builder()
+                    .status(StatusCode::FOUND)
+                    .header(LOCATION, "/bar")
+                    .body(Body::empty())
+                    .unwrap(),
+            );
+        }
+
+        let e = do_request().await.unwrap_err().to_string();
+        assert!(e.ends_with("too many redirects"), "{}", e);
+
+        // Handles redirect missing location
+        mock.push(
+            Response::builder()
+                .status(StatusCode::FOUND)
+                .body(Body::empty())
+                .unwrap(),
+        );
+
+        let e = do_request().await.unwrap_err();
+        assert_eq!(e.message, "Received redirect without LOCATION, this 
normally indicates an incorrectly configured region");
+
         // Gives up after the retrying the specified number of times
         for _ in 0..=retry.max_retries {
             mock.push(

Reply via email to