-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(crates-io): expose HTTP headers and Error type #12310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6805944
feat(crates-io): expose headers for `ResponseError::Api`
weihanglo 00a8727
fix(crates-io): don't let server sneak extra lines anywhere
weihanglo ed6692b
refactor(crates-io): rename `ResponseError` to `Error`
weihanglo a561e8b
feat(crates-io): use our own `Result` wrapper everywhere
weihanglo b4499af
refactor(crates-io): remove `anyhow`
weihanglo 31b500c
refactor(crates-io): use `thiserror`
weihanglo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,18 @@ | ||
#![allow(clippy::all)] | ||
|
||
use std::collections::BTreeMap; | ||
use std::fmt; | ||
use std::fs::File; | ||
use std::io::prelude::*; | ||
use std::io::{Cursor, SeekFrom}; | ||
use std::time::Instant; | ||
|
||
use anyhow::{bail, format_err, Context, Result}; | ||
use curl::easy::{Easy, List}; | ||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; | ||
use serde::{Deserialize, Serialize}; | ||
use url::Url; | ||
|
||
pub type Result<T> = std::result::Result<T, Error>; | ||
|
||
pub struct Registry { | ||
/// The base URL for issuing API requests. | ||
host: String, | ||
|
@@ -125,67 +125,62 @@ struct Crates { | |
meta: TotalCrates, | ||
} | ||
|
||
#[derive(Debug)] | ||
pub enum ResponseError { | ||
Curl(curl::Error), | ||
/// Error returned when interacting with a registry. | ||
#[derive(Debug, thiserror::Error)] | ||
pub enum Error { | ||
/// Error from libcurl. | ||
#[error(transparent)] | ||
Curl(#[from] curl::Error), | ||
|
||
/// Error from seriailzing the request payload and deserialzing the | ||
/// response body (like response body didn't match expected structure). | ||
#[error(transparent)] | ||
Json(#[from] serde_json::Error), | ||
|
||
/// Error from IO. Mostly from reading the tarball to upload. | ||
#[error("failed to seek tarball")] | ||
Io(#[from] std::io::Error), | ||
|
||
/// Response body was not valid utf8. | ||
#[error("invalid response body from server")] | ||
Utf8(#[from] std::string::FromUtf8Error), | ||
|
||
/// Error from API response containing JSON field `errors.details`. | ||
#[error( | ||
"the remote server responded with an error{}: {}", | ||
status(*code), | ||
errors.join(", "), | ||
)] | ||
Api { | ||
code: u32, | ||
headers: Vec<String>, | ||
errors: Vec<String>, | ||
}, | ||
|
||
/// Error from API response which didn't have pre-programmed `errors.details`. | ||
#[error( | ||
"failed to get a 200 OK response, got {code}\nheaders:\n\t{}\nbody:\n{body}", | ||
headers.join("\n\t"), | ||
)] | ||
Code { | ||
code: u32, | ||
headers: Vec<String>, | ||
body: String, | ||
}, | ||
Other(anyhow::Error), | ||
} | ||
|
||
impl std::error::Error for ResponseError { | ||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | ||
match self { | ||
ResponseError::Curl(..) => None, | ||
ResponseError::Api { .. } => None, | ||
ResponseError::Code { .. } => None, | ||
ResponseError::Other(e) => Some(e.as_ref()), | ||
} | ||
} | ||
} | ||
|
||
impl fmt::Display for ResponseError { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
match self { | ||
ResponseError::Curl(e) => write!(f, "{}", e), | ||
ResponseError::Api { code, errors } => { | ||
f.write_str("the remote server responded with an error")?; | ||
if *code != 200 { | ||
write!(f, " (status {} {})", code, reason(*code))?; | ||
}; | ||
write!(f, ": {}", errors.join(", ")) | ||
} | ||
ResponseError::Code { | ||
code, | ||
headers, | ||
body, | ||
} => write!( | ||
f, | ||
"failed to get a 200 OK response, got {}\n\ | ||
headers:\n\ | ||
\t{}\n\ | ||
body:\n\ | ||
{}", | ||
code, | ||
headers.join("\n\t"), | ||
body | ||
), | ||
ResponseError::Other(..) => write!(f, "invalid response from server"), | ||
} | ||
} | ||
} | ||
|
||
impl From<curl::Error> for ResponseError { | ||
fn from(error: curl::Error) -> Self { | ||
ResponseError::Curl(error) | ||
} | ||
/// Reason why the token was invalid. | ||
#[error("{0}")] | ||
InvalidToken(&'static str), | ||
|
||
/// Server was unavailable and timeouted. Happened when uploading a way | ||
/// too large tarball to crates.io. | ||
#[error( | ||
"Request timed out after 30 seconds. If you're trying to \ | ||
upload a crate it may be too large. If the crate is under \ | ||
10MB in size, you can email [email protected] for assistance.\n\ | ||
Total size was {0}." | ||
)] | ||
Timeout(u64), | ||
} | ||
|
||
impl Registry { | ||
|
@@ -221,10 +216,9 @@ impl Registry { | |
} | ||
|
||
fn token(&self) -> Result<&str> { | ||
let token = match self.token.as_ref() { | ||
Some(s) => s, | ||
None => bail!("no upload token found, please run `cargo login`"), | ||
}; | ||
let token = self.token.as_ref().ok_or_else(|| { | ||
Error::InvalidToken("no upload token found, please run `cargo login`") | ||
})?; | ||
check_token(token)?; | ||
Ok(token) | ||
} | ||
|
@@ -270,12 +264,8 @@ impl Registry { | |
// This checks the length using seeking instead of metadata, because | ||
// on some filesystems, getting the metadata will fail because | ||
// the file was renamed in ops::package. | ||
let tarball_len = tarball | ||
.seek(SeekFrom::End(0)) | ||
.with_context(|| "failed to seek tarball")?; | ||
tarball | ||
.seek(SeekFrom::Start(0)) | ||
.with_context(|| "failed to seek tarball")?; | ||
let tarball_len = tarball.seek(SeekFrom::End(0))?; | ||
tarball.seek(SeekFrom::Start(0))?; | ||
let header = { | ||
let mut w = Vec::new(); | ||
w.extend(&(json.len() as u32).to_le_bytes()); | ||
|
@@ -300,18 +290,12 @@ impl Registry { | |
let body = self | ||
.handle(&mut |buf| body.read(buf).unwrap_or(0)) | ||
.map_err(|e| match e { | ||
ResponseError::Code { code, .. } | ||
Error::Code { code, .. } | ||
if code == 503 | ||
&& started.elapsed().as_secs() >= 29 | ||
&& self.host_is_crates_io() => | ||
{ | ||
format_err!( | ||
"Request timed out after 30 seconds. If you're trying to \ | ||
upload a crate it may be too large. If the crate is under \ | ||
10MB in size, you can email [email protected] for assistance.\n\ | ||
Total size was {}.", | ||
tarball_len | ||
) | ||
Error::Timeout(tarball_len) | ||
} | ||
_ => e.into(), | ||
})?; | ||
|
@@ -410,10 +394,7 @@ impl Registry { | |
} | ||
} | ||
|
||
fn handle( | ||
&mut self, | ||
read: &mut dyn FnMut(&mut [u8]) -> usize, | ||
) -> std::result::Result<String, ResponseError> { | ||
fn handle(&mut self, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result<String> { | ||
let mut headers = Vec::new(); | ||
let mut body = Vec::new(); | ||
{ | ||
|
@@ -427,28 +408,29 @@ impl Registry { | |
// Headers contain trailing \r\n, trim them to make it easier | ||
// to work with. | ||
let s = String::from_utf8_lossy(data).trim().to_string(); | ||
// Don't let server sneak extra lines anywhere. | ||
if s.contains('\n') { | ||
return true; | ||
} | ||
headers.push(s); | ||
true | ||
})?; | ||
handle.perform()?; | ||
} | ||
|
||
let body = match String::from_utf8(body) { | ||
Ok(body) => body, | ||
Err(..) => { | ||
return Err(ResponseError::Other(format_err!( | ||
"response body was not valid utf-8" | ||
))) | ||
} | ||
}; | ||
let body = String::from_utf8(body)?; | ||
let errors = serde_json::from_str::<ApiErrorList>(&body) | ||
.ok() | ||
.map(|s| s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>()); | ||
|
||
match (self.handle.response_code()?, errors) { | ||
(0, None) | (200, None) => Ok(body), | ||
(code, Some(errors)) => Err(ResponseError::Api { code, errors }), | ||
(code, None) => Err(ResponseError::Code { | ||
(code, Some(errors)) => Err(Error::Api { | ||
code, | ||
headers, | ||
errors, | ||
}), | ||
(code, None) => Err(Error::Code { | ||
code, | ||
headers, | ||
body, | ||
|
@@ -457,6 +439,15 @@ impl Registry { | |
} | ||
} | ||
|
||
fn status(code: u32) -> String { | ||
if code == 200 { | ||
String::new() | ||
} else { | ||
let reason = reason(code); | ||
format!(" (status {code} {reason})") | ||
} | ||
} | ||
|
||
fn reason(code: u32) -> &'static str { | ||
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status | ||
match code { | ||
|
@@ -520,7 +511,7 @@ pub fn is_url_crates_io(url: &str) -> bool { | |
/// registries only create tokens in that format so that is as less restricted as possible. | ||
pub fn check_token(token: &str) -> Result<()> { | ||
if token.is_empty() { | ||
bail!("please provide a non-empty token"); | ||
return Err(Error::InvalidToken("please provide a non-empty token")); | ||
} | ||
if token.bytes().all(|b| { | ||
// This is essentially the US-ASCII limitation of | ||
|
@@ -531,9 +522,9 @@ pub fn check_token(token: &str) -> Result<()> { | |
}) { | ||
Ok(()) | ||
} else { | ||
Err(anyhow::anyhow!( | ||
Err(Error::InvalidToken( | ||
"token contains invalid characters.\nOnly printable ISO-8859-1 characters \ | ||
are allowed as it is sent in a HTTPS header." | ||
are allowed as it is sent in a HTTPS header.", | ||
)) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.