diff --git a/crates/skilld-command/src/remote.rs b/crates/skilld-command/src/remote.rs index a9c71785..3a7e26a7 100644 --- a/crates/skilld-command/src/remote.rs +++ b/crates/skilld-command/src/remote.rs @@ -852,7 +852,10 @@ impl SkilldRemote { continue; } if !(200..300).contains(&response.status) { - return Err(problem_error(&response)); + return Err(match allowed { + AllowedOrigin::Github => github_error(&response), + AllowedOrigin::Service(_) => problem_error(&response), + }); } return Ok(response); } @@ -2486,6 +2489,19 @@ fn problem_error(response: &HttpResponse) -> RemoteError { ) } +fn github_error(response: &HttpResponse) -> RemoteError { + if !is_rate_limited(response) { + return service_unavailable_error(response); + } + let detail = match bounded_rate_limit_wait(response) { + Some(seconds) => { + format!("GitHub rate limited this request. Retry after {seconds} seconds.") + } + None => "GitHub rate limited this request. Retry in a minute.".to_owned(), + }; + RemoteError::new("RATE_LIMITED", detail) +} + fn service_unavailable_error(response: &HttpResponse) -> RemoteError { match (response.status, retry_after_seconds(response)) { (429, Some(seconds)) => RemoteError::new( diff --git a/crates/skilld-command/tests/remote.rs b/crates/skilld-command/tests/remote.rs index 6df761e1..19019bd0 100644 --- a/crates/skilld-command/tests/remote.rs +++ b/crates/skilld-command/tests/remote.rs @@ -1846,6 +1846,40 @@ fn direct_github_access_resolves_an_exact_public_commit_without_tokens() { })); } +#[test] +fn a_direct_github_rate_limit_names_github_and_the_reset_wait() { + let reset = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + + 300; + let mut limited = response(403, br#"{"message":"API rate limit exceeded"}"#.to_vec()); + limited + .headers + .insert("x-ratelimit-remaining".to_owned(), "0".to_owned()); + limited + .headers + .insert("x-ratelimit-reset".to_owned(), reset.to_string()); + let http = Arc::new(FakeHttp::with([limited])); + let remote = SkilldRemote::new( + http.clone(), + Arc::new(NoTokenProvider), + NativeRemoteConfig::Unconfigured, + ) + .with_sleeper(Arc::new(NoSleep)); + let selector = RemoteSelector::parse("github:skilld-dev/skills/skills/example").unwrap(); + + let error = remote.prepare(&selector, true).unwrap_err(); + + assert_eq!(error.code, "RATE_LIMITED"); + assert!( + error + .message + .starts_with("GitHub rate limited this request. Retry after ") + ); + assert_eq!(http.requests.lock().unwrap().len(), 1); +} + #[test] fn prepare_exact_rejects_a_conflicting_selector_commit_before_http() { let selector_commit = "0123456789abcdef0123456789abcdef01234567";