pub mod account_client;
pub mod client;
pub mod filter_client;
pub mod gain_client;
pub mod http;
pub mod machine;
pub mod publish_client;
pub mod retry;
pub mod sync_client;
pub mod tos_client;
use std::fmt;
pub fn is_debug() -> bool {
crate::paths::debug_enabled()
}
#[derive(Debug)]
pub enum RemoteError {
ConnectionFailed { url: String, source: reqwest::Error },
Timeout { url: String, source: reqwest::Error },
ServerError {
url: String,
status: reqwest::StatusCode,
body: String,
},
Unauthorized,
RateLimited(RateLimitedError),
RequestError { url: String, source: reqwest::Error },
ClientError {
url: String,
status: reqwest::StatusCode,
body: String,
},
}
impl fmt::Display for RemoteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ConnectionFailed { url, source } => {
if is_debug() {
write!(f, "remote: could not connect to {url}: {source}")
} else {
write!(
f,
"remote: could not connect to server (use TOKF_DEBUG=1 for details)"
)
}
}
Self::Timeout { url, source } => {
if is_debug() {
write!(f, "remote: request to {url} timed out: {source}")
} else {
write!(
f,
"remote: request timed out (use TOKF_DEBUG=1 for details)"
)
}
}
Self::ServerError { url, status, body } => {
if is_debug() {
write!(f, "remote: server error {status} from {url}: {body}")
} else {
write!(
f,
"remote: server error {status} (use TOKF_DEBUG=1 for details)"
)
}
}
Self::Unauthorized => {
write!(
f,
"remote: HTTP 401 Unauthorized — run `tokf auth login` to re-authenticate"
)
}
Self::RequestError { url, source } => {
if is_debug() {
write!(f, "remote: request error for {url}: {source}")
} else {
write!(f, "remote: request error (use TOKF_DEBUG=1 for details)")
}
}
Self::RateLimited(inner) => write!(f, "remote: {inner}"),
Self::ClientError { url, status, body } => {
if is_debug() {
write!(f, "remote: HTTP {status} from {url}: {body}")
} else {
write!(f, "remote: HTTP {status} (use TOKF_DEBUG=1 for details)")
}
}
}
}
}
impl std::error::Error for RemoteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ConnectionFailed { source, .. }
| Self::Timeout { source, .. }
| Self::RequestError { source, .. } => Some(source),
Self::RateLimited(inner) => Some(inner),
_ => None,
}
}
}
impl RemoteError {
pub const fn is_transient(&self) -> bool {
matches!(
self,
Self::ConnectionFailed { .. } | Self::Timeout { .. } | Self::ServerError { .. }
)
}
}
#[derive(Debug)]
pub struct RateLimitedError {
pub retry_after_secs: u64,
}
impl std::fmt::Display for RateLimitedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"rate limit exceeded — try again in {}s (HTTP 429)",
self.retry_after_secs
)
}
}
impl std::error::Error for RateLimitedError {}
pub(crate) fn classify_reqwest_error(url: &str, err: reqwest::Error) -> RemoteError {
if err.is_timeout() {
RemoteError::Timeout {
url: url.to_string(),
source: err,
}
} else if err.is_connect() {
RemoteError::ConnectionFailed {
url: url.to_string(),
source: err,
}
} else if err.is_request() || err.is_builder() || err.is_redirect() {
RemoteError::RequestError {
url: url.to_string(),
source: err,
}
} else {
RemoteError::ConnectionFailed {
url: url.to_string(),
source: err,
}
}
}
pub(crate) fn check_auth_and_rate_limit(
resp: &reqwest::blocking::Response,
) -> Result<(), RemoteError> {
let status = resp.status();
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err(RemoteError::Unauthorized);
}
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
let retry_after = resp
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(60);
return Err(RemoteError::RateLimited(RateLimitedError {
retry_after_secs: retry_after,
}));
}
Ok(())
}
pub(crate) fn require_success(
resp: reqwest::blocking::Response,
url: &str,
) -> Result<reqwest::blocking::Response, RemoteError> {
let status = resp.status();
if status.is_success() {
return Ok(resp);
}
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err(RemoteError::Unauthorized);
}
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
let retry_after = resp
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(60);
return Err(RemoteError::RateLimited(RateLimitedError {
retry_after_secs: retry_after,
}));
}
let body = resp.text().unwrap_or_default();
if status.is_server_error() {
return Err(RemoteError::ServerError {
url: url.to_string(),
status,
body,
});
}
Err(RemoteError::ClientError {
url: url.to_string(),
status,
body,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::error::Error as _;
use super::*;
#[test]
fn is_debug_returns_false_by_default() {
let _ = is_debug();
}
#[test]
fn remote_error_display_unauthorized() {
let err = RemoteError::Unauthorized;
let msg = err.to_string();
assert!(msg.contains("401 Unauthorized"));
assert!(msg.contains("tokf auth login"));
}
#[test]
fn remote_error_display_rate_limited() {
let err = RemoteError::RateLimited(RateLimitedError {
retry_after_secs: 30,
});
let msg = err.to_string();
assert!(msg.contains("rate limit"));
assert!(msg.contains("30s"));
}
#[test]
fn remote_error_display_server_error_no_debug() {
let err = RemoteError::ServerError {
url: "https://api.tokf.net/api/test".to_string(),
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
body: "internal error".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("500"));
assert!(msg.contains("TOKF_DEBUG=1"));
}
#[test]
fn remote_error_display_client_error_no_debug() {
let err = RemoteError::ClientError {
url: "https://api.tokf.net/api/test".to_string(),
status: reqwest::StatusCode::NOT_FOUND,
body: "not found".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("404"));
assert!(msg.contains("TOKF_DEBUG=1"));
}
#[test]
fn remote_error_is_transient() {
assert!(
RemoteError::ServerError {
url: String::new(),
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
body: String::new(),
}
.is_transient()
);
assert!(!RemoteError::Unauthorized.is_transient());
assert!(
!RemoteError::RateLimited(RateLimitedError {
retry_after_secs: 0
})
.is_transient()
);
}
#[test]
fn rate_limited_error_display() {
let err = RateLimitedError {
retry_after_secs: 120,
};
assert_eq!(
err.to_string(),
"rate limit exceeded — try again in 120s (HTTP 429)"
);
}
#[test]
fn remote_error_display_client_error_bad_request() {
let err = RemoteError::ClientError {
url: "https://api.tokf.net/bad".to_string(),
status: reqwest::StatusCode::BAD_REQUEST,
body: "bad request".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("400"));
assert!(msg.contains("TOKF_DEBUG=1"));
}
#[test]
fn request_error_is_not_transient() {
assert!(
!RemoteError::ClientError {
url: String::new(),
status: reqwest::StatusCode::BAD_REQUEST,
body: String::new(),
}
.is_transient()
);
}
#[test]
fn remote_error_source_chain() {
assert!(RemoteError::Unauthorized.source().is_none());
let rate_err = RemoteError::RateLimited(RateLimitedError {
retry_after_secs: 5,
});
assert!(rate_err.source().is_some());
assert!(
RemoteError::ClientError {
url: String::new(),
status: reqwest::StatusCode::NOT_FOUND,
body: String::new(),
}
.source()
.is_none()
);
}
}