Skip to content

Commit 958f4a1

Browse files
committed
Auto merge of #6936 - sgrif:sg-timeout-error, r=alexcrichton
Give a better error message when crates.io requests time out crates.io is hosted on Heroku, which means we have a hard 30 second time limit on all requests. Typically this is only hit when someone is attempting to upload a crate so large that it would have been eventually rejected anyway, but it can also happen if a user is on a very slow internet connection. When this happens, the request is terminated by the platform and we have no control over the response that gets sent. This results in the user getting a very unhelpful error message from Cargo, and some generic error page HTML spat out into their terminal. We could work around this on our end by adding a 29 second timeout *somewhere else* in the stack, but we have a lot of layers that buffer requests to protect against slow client attacks, and it'd be a pretty decent amount of work. Since we eventually want to switch over to having Cargo do the S3 upload instead of us, it doesn't make sense to spend so much time on an error scenario that eventually will go away. I've tried to keep this uncoupled from crates.io as much as possible, since alternate registries might not be hosted on Heroku or have the same restricitions. But I figure "a 503 that took more than 30 seconds" is a safe bet on this being hit. If we're ok with coupling this to crates.io, I'd like to include "If your crate is less than 10MB you can email help@crates.io for assistance" in the error message. Ref rust-lang/crates.io#1709
2 parents 414c1eb + 4d93f2d commit 958f4a1

File tree

2 files changed

+61
-51
lines changed

2 files changed

+61
-51
lines changed

src/cargo/ops/registry.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,7 @@ fn verify_dependencies(
129129
// This extra hostname check is mostly to assist with testing,
130130
// but also prevents someone using `--index` to specify
131131
// something that points to crates.io.
132-
let is_crates_io = registry
133-
.host()
134-
.to_url()
135-
.map(|u| u.host_str() == Some("crates.io"))
136-
.unwrap_or(false);
137-
if registry_src.is_default_registry() || is_crates_io {
132+
if registry_src.is_default_registry() || registry.host_is_crates_io() {
138133
bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
139134
registries either publish `{}` on crates.io or pull it into this repository\n\
140135
and specify it with a path and version\n\

src/crates-io/lib.rs

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ use std::collections::BTreeMap;
55
use std::fs::File;
66
use std::io::prelude::*;
77
use std::io::Cursor;
8+
use std::time::Instant;
89

910
use curl::easy::{Easy, List};
1011
use failure::bail;
1112
use http::status::StatusCode;
1213
use serde::{Deserialize, Serialize};
1314
use serde_json;
1415
use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET};
16+
use url::Url;
1517

1618
pub type Result<T> = std::result::Result<T, failure::Error>;
1719

@@ -141,6 +143,12 @@ impl Registry {
141143
&self.host
142144
}
143145

146+
pub fn host_is_crates_io(&self) -> bool {
147+
Url::parse(self.host())
148+
.map(|u| u.host_str() == Some("crates.io"))
149+
.unwrap_or(false)
150+
}
151+
144152
pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result<String> {
145153
let body = serde_json::to_string(&OwnersReq { users: owners })?;
146154
let body = self.put(&format!("/crates/{}/owners", krate), body.as_bytes())?;
@@ -207,7 +215,7 @@ impl Registry {
207215
headers.append(&format!("Authorization: {}", token))?;
208216
self.handle.http_headers(headers)?;
209217

210-
let body = handle(&mut self.handle, &mut |buf| body.read(buf).unwrap_or(0))?;
218+
let body = self.handle(&mut |buf| body.read(buf).unwrap_or(0))?;
211219

212220
let response = if body.is_empty() {
213221
"{}".parse()?
@@ -300,55 +308,62 @@ impl Registry {
300308
Some(mut body) => {
301309
self.handle.upload(true)?;
302310
self.handle.in_filesize(body.len() as u64)?;
303-
handle(&mut self.handle, &mut |buf| body.read(buf).unwrap_or(0))
311+
self.handle(&mut |buf| body.read(buf).unwrap_or(0))
304312
}
305-
None => handle(&mut self.handle, &mut |_| 0),
313+
None => self.handle(&mut |_| 0),
306314
}
307315
}
308-
}
309316

310-
fn handle(handle: &mut Easy, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result<String> {
311-
let mut headers = Vec::new();
312-
let mut body = Vec::new();
313-
{
314-
let mut handle = handle.transfer();
315-
handle.read_function(|buf| Ok(read(buf)))?;
316-
handle.write_function(|data| {
317-
body.extend_from_slice(data);
318-
Ok(data.len())
319-
})?;
320-
handle.header_function(|data| {
321-
headers.push(String::from_utf8_lossy(data).into_owned());
322-
true
323-
})?;
324-
handle.perform()?;
325-
}
317+
fn handle(&mut self, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result<String> {
318+
let mut headers = Vec::new();
319+
let mut body = Vec::new();
320+
let started;
321+
{
322+
let mut handle = self.handle.transfer();
323+
handle.read_function(|buf| Ok(read(buf)))?;
324+
handle.write_function(|data| {
325+
body.extend_from_slice(data);
326+
Ok(data.len())
327+
})?;
328+
handle.header_function(|data| {
329+
headers.push(String::from_utf8_lossy(data).into_owned());
330+
true
331+
})?;
332+
started = Instant::now();
333+
handle.perform()?;
334+
}
326335

327-
let body = match String::from_utf8(body) {
328-
Ok(body) => body,
329-
Err(..) => bail!("response body was not valid utf-8"),
330-
};
331-
let errors = serde_json::from_str::<ApiErrorList>(&body).ok().map(|s| {
332-
s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>()
333-
});
334-
335-
match (handle.response_code()?, errors) {
336-
(0, None) | (200, None) => {},
337-
(code, Some(errors)) => {
338-
let code = StatusCode::from_u16(code as _)?;
339-
bail!("api errors (status {}): {}", code, errors.join(", "))
336+
let body = match String::from_utf8(body) {
337+
Ok(body) => body,
338+
Err(..) => bail!("response body was not valid utf-8"),
339+
};
340+
let errors = serde_json::from_str::<ApiErrorList>(&body).ok().map(|s| {
341+
s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>()
342+
});
343+
344+
match (self.handle.response_code()?, errors) {
345+
(0, None) | (200, None) => {},
346+
(503, None) if started.elapsed().as_secs() >= 29 && self.host_is_crates_io() => {
347+
bail!("Request timed out after 30 seconds. If you're trying to \
348+
upload a crate it may be too large. If the crate is under \
349+
10MB in size, you can email help@crates.io for assistance.")
350+
}
351+
(code, Some(errors)) => {
352+
let code = StatusCode::from_u16(code as _)?;
353+
bail!("api errors (status {}): {}", code, errors.join(", "))
354+
}
355+
(code, None) => bail!(
356+
"failed to get a 200 OK response, got {}\n\
357+
headers:\n\
358+
\t{}\n\
359+
body:\n\
360+
{}",
361+
code,
362+
headers.join("\n\t"),
363+
body,
364+
),
340365
}
341-
(code, None) => bail!(
342-
"failed to get a 200 OK response, got {}\n\
343-
headers:\n\
344-
\t{}\n\
345-
body:\n\
346-
{}",
347-
code,
348-
headers.join("\n\t"),
349-
body,
350-
),
351-
}
352366

353-
Ok(body)
367+
Ok(body)
368+
}
354369
}

0 commit comments

Comments
 (0)