Skip to content

Commit 8713dcb

Browse files
committed
Avoid retrying object_exists 404 responses
1 parent 57525a4 commit 8713dcb

5 files changed

Lines changed: 198 additions & 11 deletions

File tree

s3/src/bucket.rs

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,17 +1025,8 @@ impl Bucket {
10251025
pub async fn object_exists<S: AsRef<str>>(&self, path: S) -> Result<bool, S3Error> {
10261026
let command = Command::HeadObject;
10271027
let request = RequestImpl::new(self, path.as_ref(), command).await?;
1028-
let response_data = match request.response_data(false).await {
1029-
Ok(response_data) => response_data,
1030-
Err(S3Error::HttpFailWithBody(status_code, error)) => {
1031-
if status_code == 404 {
1032-
return Ok(false);
1033-
}
1034-
return Err(S3Error::HttpFailWithBody(status_code, error));
1035-
}
1036-
Err(e) => return Err(e),
1037-
};
1038-
Ok(response_data.status_code() != 404)
1028+
let status_code = request.response_status().await?;
1029+
Ok(status_code != 404)
10391030
}
10401031

10411032
#[maybe_async::maybe_async]
@@ -3113,11 +3104,75 @@ mod test {
31133104
use crate::{Bucket, PostPolicy};
31143105
use http::header::{CACHE_CONTROL, HeaderMap, HeaderName, HeaderValue};
31153106
use std::env;
3107+
#[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3108+
use std::io::{Read, Write};
3109+
#[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3110+
use std::net::TcpListener;
3111+
#[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3112+
use std::sync::{
3113+
Arc,
3114+
atomic::{AtomicUsize, Ordering},
3115+
};
3116+
#[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3117+
use std::thread;
31163118

31173119
fn init() {
31183120
let _ = env_logger::builder().is_test(true).try_init();
31193121
}
31203122

3123+
#[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3124+
#[tokio::test]
3125+
async fn test_object_exists_404_does_not_retry() {
3126+
init();
3127+
3128+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3129+
let endpoint = format!("http://{}", listener.local_addr().unwrap());
3130+
let requests = Arc::new(AtomicUsize::new(0));
3131+
let request_count = Arc::clone(&requests);
3132+
3133+
let server = thread::spawn(move || {
3134+
let (mut stream, _) = listener.accept().unwrap();
3135+
request_count.fetch_add(1, Ordering::SeqCst);
3136+
3137+
let mut buffer = [0; 2048];
3138+
let _ = stream.read(&mut buffer).unwrap();
3139+
stream
3140+
.write_all(
3141+
b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
3142+
)
3143+
.unwrap();
3144+
});
3145+
3146+
crate::set_retries(1);
3147+
3148+
let credentials = Credentials::new(
3149+
Some("test_access_key"),
3150+
Some("test_secret_key"),
3151+
None,
3152+
None,
3153+
None,
3154+
)
3155+
.unwrap();
3156+
let bucket = Bucket::new(
3157+
"test-bucket",
3158+
Region::Custom {
3159+
region: "us-east-1".to_owned(),
3160+
endpoint,
3161+
},
3162+
credentials,
3163+
)
3164+
.unwrap()
3165+
.with_path_style();
3166+
3167+
let exists = bucket.object_exists("/missing.txt").await.unwrap();
3168+
3169+
crate::set_retries(1);
3170+
server.join().unwrap();
3171+
3172+
assert!(!exists);
3173+
assert_eq!(requests.load(Ordering::SeqCst), 1);
3174+
}
3175+
31213176
fn test_aws_credentials() -> Credentials {
31223177
Credentials::new(
31233178
Some(&env::var("EU_AWS_ACCESS_KEY_ID").unwrap()),

s3/src/request/async_std_backend.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,47 @@ impl<'a> Request for SurfRequest<'a> {
8484
Ok(response)
8585
}
8686

87+
async fn response_status(&self) -> Result<u16, S3Error> {
88+
crate::retry! {
89+
async {
90+
let headers = self.headers().await?;
91+
92+
let request = match self.command.http_verb() {
93+
HttpMethod::Get => surf::Request::builder(Method::Get, self.url()?),
94+
HttpMethod::Delete => surf::Request::builder(Method::Delete, self.url()?),
95+
HttpMethod::Put => surf::Request::builder(Method::Put, self.url()?),
96+
HttpMethod::Post => surf::Request::builder(Method::Post, self.url()?),
97+
HttpMethod::Head => surf::Request::builder(Method::Head, self.url()?),
98+
};
99+
100+
let mut request = request.body(self.request_body()?);
101+
102+
for (name, value) in headers.iter() {
103+
request = request.header(
104+
HeaderName::from_bytes(AsRef::<[u8]>::as_ref(&name).to_vec())
105+
.expect("Could not parse heaeder name"),
106+
HeaderValue::from_bytes(AsRef::<[u8]>::as_ref(&value).to_vec())
107+
.expect("Could not parse header value"),
108+
);
109+
}
110+
111+
let response = request
112+
.send()
113+
.await
114+
.map_err(|e| S3Error::Surf(e.to_string()))?;
115+
let status = u16::from(response.status());
116+
117+
if status == 404 {
118+
Ok(status)
119+
} else if cfg!(feature = "fail-on-err") && !response.status().is_success() {
120+
Err(S3Error::HttpFail)
121+
} else {
122+
Ok(status)
123+
}
124+
}.await
125+
}
126+
}
127+
87128
async fn response_data(&self, etag: bool) -> Result<ResponseData, S3Error> {
88129
let mut response = crate::retry! {self.response().await}?;
89130
let status_code = response.status();

s3/src/request/blocking.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,43 @@ impl<'a> Request for AttoRequest<'a> {
7979
Ok(response)
8080
}
8181

82+
fn response_status(&self) -> Result<u16, S3Error> {
83+
crate::retry! {
84+
{
85+
let headers = self.headers()?;
86+
let mut session = attohttpc::Session::new();
87+
88+
for (name, value) in headers.iter() {
89+
session.header(HeaderName::from_bytes(name.as_ref())?, value.to_str()?);
90+
}
91+
92+
if let Some(timeout) = self.bucket.request_timeout {
93+
session.timeout(timeout)
94+
}
95+
96+
let request = match self.command.http_verb() {
97+
HttpMethod::Get => session.get(self.url()?),
98+
HttpMethod::Delete => session.delete(self.url()?),
99+
HttpMethod::Put => session.put(self.url()?),
100+
HttpMethod::Post => session.post(self.url()?),
101+
HttpMethod::Head => session.head(self.url()?),
102+
};
103+
104+
let response = request.bytes(&self.request_body()?).send()?;
105+
let status = response.status().as_u16();
106+
107+
if status == 404 {
108+
Ok(status)
109+
} else if cfg!(feature = "fail-on-err") && !response.status().is_success() {
110+
let text = response.text()?;
111+
Err(S3Error::HttpFailWithBody(status, text))
112+
} else {
113+
Ok(status)
114+
}
115+
}
116+
}
117+
}
118+
82119
fn response_data(&self, etag: bool) -> Result<ResponseData, S3Error> {
83120
let response = crate::retry! {self.response()}?;
84121
let status_code = response.status().as_u16();

s3/src/request/request_trait.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ pub trait Request {
210210
#[cfg(any(feature = "with-async-std", feature = "with-tokio"))]
211211
async fn response_data_to_stream(&self) -> Result<ResponseDataStream, S3Error>;
212212
async fn response_header(&self) -> Result<(Self::HeaderMap, u16), S3Error>;
213+
async fn response_status(&self) -> Result<u16, S3Error> {
214+
let (_, status_code) = self.response_header().await?;
215+
Ok(status_code)
216+
}
213217
fn datetime(&self) -> OffsetDateTime;
214218
fn bucket(&self) -> Bucket;
215219
fn command(&self) -> Command<'_>;

s3/src/request/tokio_backend.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,56 @@ impl<'a> Request for ReqwestRequest<'a> {
117117
Ok(response)
118118
}
119119

120+
async fn response_status(&self) -> Result<u16, S3Error> {
121+
retry! {
122+
async {
123+
let headers = self
124+
.headers()
125+
.await?
126+
.iter()
127+
.map(|(k, v)| {
128+
(
129+
reqwest::header::HeaderName::from_str(k.as_str()),
130+
reqwest::header::HeaderValue::from_str(v.to_str().unwrap_or_default()),
131+
)
132+
})
133+
.filter(|(k, v)| k.is_ok() && v.is_ok())
134+
.map(|(k, v)| (k.unwrap(), v.unwrap()))
135+
.collect();
136+
137+
let client = self.bucket.http_client();
138+
139+
let method = match self.command.http_verb() {
140+
HttpMethod::Delete => reqwest::Method::DELETE,
141+
HttpMethod::Get => reqwest::Method::GET,
142+
HttpMethod::Post => reqwest::Method::POST,
143+
HttpMethod::Put => reqwest::Method::PUT,
144+
HttpMethod::Head => reqwest::Method::HEAD,
145+
};
146+
147+
let request = client
148+
.request(method, self.url()?.as_str())
149+
.headers(headers)
150+
.body(self.request_body()?);
151+
152+
let request = request.build()?;
153+
let response = client.execute(request).await?;
154+
let status = response.status().as_u16();
155+
156+
if status == 404 {
157+
return Ok(status);
158+
}
159+
160+
if cfg!(feature = "fail-on-err") && !response.status().is_success() {
161+
let text = response.text().await?;
162+
return Err(S3Error::HttpFailWithBody(status, text));
163+
}
164+
165+
Ok(status)
166+
}.await
167+
}
168+
}
169+
120170
async fn response_data(&self, etag: bool) -> Result<ResponseData, S3Error> {
121171
let response = retry! {self.response().await }?;
122172
let status_code = response.status().as_u16();

0 commit comments

Comments
 (0)