use super::secondary_rate_limit::is_secondary_rate_limit;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UploadAttemptOutcome {
Success,
AlreadyExists,
SecondaryRateLimited,
PrimaryRateLimited,
NotFound,
TransientRetry,
Fatal,
}
pub(crate) fn classify_upload_attempt<T>(
result: &Result<T, octocrab::Error>,
) -> UploadAttemptOutcome {
let err = match result {
Ok(_) => return UploadAttemptOutcome::Success,
Err(e) => e,
};
let is_already_exists = matches!(
err,
octocrab::Error::GitHub { source, .. }
if source.status_code.as_u16() == 422
&& source.errors.as_ref().is_some_and(|errs| {
errs.iter().any(|e| {
e.get("code").and_then(|v| v.as_str()) == Some("already_exists")
})
})
);
if is_already_exists {
return UploadAttemptOutcome::AlreadyExists;
}
let is_not_found = matches!(
err,
octocrab::Error::GitHub { source, .. }
if source.status_code.as_u16() == 404
);
if is_not_found {
return UploadAttemptOutcome::NotFound;
}
if is_secondary_rate_limit(err) {
return UploadAttemptOutcome::SecondaryRateLimited;
}
let is_primary_rate_limited = matches!(
err,
octocrab::Error::GitHub { source, .. }
if source.status_code.as_u16() == 403
|| source.status_code.as_u16() == 429
);
if is_primary_rate_limited {
return UploadAttemptOutcome::PrimaryRateLimited;
}
let is_transient_status = matches!(
err,
octocrab::Error::GitHub { source, .. }
if source.status_code.is_server_error()
|| source.status_code.as_u16() == 401
);
if is_transient_status
|| matches!(err, octocrab::Error::Hyper { .. })
|| matches!(err, octocrab::Error::Http { .. })
|| matches!(err, octocrab::Error::Service { .. })
|| matches!(err, octocrab::Error::Other { .. })
|| matches!(err, octocrab::Error::Serde { .. })
|| matches!(err, octocrab::Error::Json { .. })
{
return UploadAttemptOutcome::TransientRetry;
}
UploadAttemptOutcome::Fatal
}
#[cfg(test)]
mod tests {
use super::*;
use anodizer_core::test_helpers::responder::spawn_oneshot_http_responder;
async fn synth_github_error(status: u16, body: &str) -> octocrab::Error {
let body_len = body.len();
let raw = format!(
"HTTP/1.1 {status} STATUS\r\n\
Content-Type: application/json\r\n\
Content-Length: {body_len}\r\n\
\r\n\
{body}"
);
let static_resp: &'static str = Box::leak(raw.into_boxed_str());
let serve_count: usize = if status == 429 || status >= 500 { 4 } else { 1 };
let (addr, _calls) = spawn_oneshot_http_responder(vec![static_resp; serve_count]);
let octo = octocrab::OctocrabBuilder::new()
.base_uri(format!("http://{addr}/"))
.expect("base_uri")
.build()
.expect("build");
octo.get::<serde_json::Value, _, _>("/test", None::<&()>)
.await
.expect_err("synth_github_error: octocrab must surface Err for non-2xx status")
}
fn ok_result() -> Result<serde_json::Value, octocrab::Error> {
Ok(serde_json::json!({}))
}
#[tokio::test]
async fn ok_classifies_as_success() {
assert_eq!(
classify_upload_attempt(&ok_result()),
UploadAttemptOutcome::Success,
);
}
#[tokio::test]
async fn github_422_already_exists_classifies_as_already_exists() {
let body = r#"{"message":"Validation Failed","errors":[{"resource":"ReleaseAsset","code":"already_exists","field":"name"}]}"#;
let err = synth_github_error(422, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::AlreadyExists,
);
}
#[tokio::test]
async fn github_422_other_code_is_fatal() {
let body = r#"{"message":"Validation Failed","errors":[{"resource":"ReleaseAsset","code":"invalid","field":"name"}]}"#;
let err = synth_github_error(422, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::Fatal,
);
}
#[tokio::test]
async fn github_404_classifies_as_not_found() {
let body = r#"{"message":"Not Found"}"#;
let err = synth_github_error(404, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::NotFound,
);
}
#[tokio::test]
async fn github_500_classifies_as_transient_retry() {
let body = r#"{"message":"Server Error"}"#;
let err = synth_github_error(500, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::TransientRetry,
);
}
#[tokio::test]
async fn github_503_classifies_as_transient_retry() {
let body = r#"{"message":"Service Unavailable"}"#;
let err = synth_github_error(503, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::TransientRetry,
);
}
#[tokio::test]
async fn github_403_with_secondary_rl_body_classifies_as_secondary() {
let body = r#"{"message":"You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.","documentation_url":"https://docs.github.com/rest/overview/resources-in-the-rest-api#secondary-rate-limits"}"#;
let err = synth_github_error(403, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::SecondaryRateLimited,
);
}
#[tokio::test]
async fn github_403_without_secondary_rl_body_classifies_as_primary() {
let body =
r#"{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}"#;
let err = synth_github_error(403, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::PrimaryRateLimited,
);
}
#[tokio::test]
async fn github_401_classifies_as_transient_retry() {
let body =
r#"{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}"#;
let err = synth_github_error(401, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::TransientRetry,
);
}
#[tokio::test]
async fn github_400_classifies_as_fatal() {
let body = r#"{"message":"Bad Request"}"#;
let err = synth_github_error(400, body).await;
let result: Result<serde_json::Value, octocrab::Error> = Err(err);
assert_eq!(
classify_upload_attempt(&result),
UploadAttemptOutcome::Fatal,
);
}
}