Skip to main content

claude_usage/
client.rs

1//! HTTP client for the Anthropic usage API.
2//!
3//! This module provides functions to fetch usage data from the Anthropic API.
4//! It handles authentication, headers, and error mapping.
5
6use crate::error::ApiError;
7
8/// Anthropic OAuth usage API endpoint.
9pub const USAGE_API_URL: &str = "https://api.anthropic.com/api/oauth/usage";
10
11/// Required beta header value for OAuth endpoints.
12pub const BETA_HEADER: &str = "oauth-2025-04-20";
13
14/// Fetch raw usage data from the Anthropic API (blocking).
15///
16/// This function makes a synchronous HTTP request to the usage API
17/// and returns the raw JSON response body.
18///
19/// # Arguments
20///
21/// * `token` - OAuth access token for authentication
22///
23/// # Errors
24///
25/// Returns [`ApiError`] if:
26/// - Network request fails
27/// - Server returns 401 (unauthorized)
28/// - Server returns 429 (rate limited)
29/// - Server returns 5xx (server error)
30/// - Server returns unexpected status code
31///
32/// # Security
33///
34/// The token is used only for this request and is not stored.
35#[cfg(feature = "blocking")]
36pub fn fetch_usage_raw(token: &str) -> Result<String, ApiError> {
37    let client = reqwest::blocking::Client::new();
38
39    let response = client
40        .get(USAGE_API_URL)
41        .header("Authorization", format!("Bearer {}", token))
42        .header("anthropic-beta", BETA_HEADER)
43        .send()
44        // Use generic message to avoid any potential token exposure in error details
45        .map_err(|_| ApiError::Network("Failed to connect to Anthropic API".to_string()))?;
46
47    map_response(response)
48}
49
50/// Map HTTP response to result, handling error status codes.
51#[cfg(feature = "blocking")]
52fn map_response(response: reqwest::blocking::Response) -> Result<String, ApiError> {
53    let status = response.status().as_u16();
54
55    match status {
56        200 => response
57            .text()
58            .map_err(|_| ApiError::Network("Failed to read response body".to_string())),
59        401 => Err(ApiError::Unauthorized),
60        429 => {
61            let retry_after = response
62                .headers()
63                .get("retry-after")
64                .and_then(|v| v.to_str().ok())
65                .map(String::from);
66            Err(ApiError::RateLimited { retry_after })
67        }
68        500..=599 => Err(ApiError::Server(status)),
69        _ => Err(ApiError::Unexpected(status)),
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_api_url_is_correct() {
79        assert_eq!(USAGE_API_URL, "https://api.anthropic.com/api/oauth/usage");
80    }
81
82    #[test]
83    fn test_beta_header_is_correct() {
84        assert_eq!(BETA_HEADER, "oauth-2025-04-20");
85    }
86
87    // Integration test - requires valid token
88    #[test]
89    #[ignore = "requires real API credentials"]
90    #[cfg(feature = "blocking")]
91    fn test_fetch_usage_raw_integration() {
92        // This test requires CLAUDE_CODE_OAUTH_TOKEN env var or real credentials
93        let token = std::env::var("CLAUDE_CODE_OAUTH_TOKEN")
94            .expect("CLAUDE_CODE_OAUTH_TOKEN must be set for integration test");
95
96        let result = fetch_usage_raw(&token);
97        match result {
98            Ok(body) => {
99                assert!(body.contains("five_hour"));
100                assert!(body.contains("seven_day"));
101                println!("API response received successfully");
102            }
103            Err(ApiError::Unauthorized) => {
104                println!("Token is invalid or expired");
105            }
106            Err(e) => {
107                panic!("Unexpected error: {}", e);
108            }
109        }
110    }
111
112    #[test]
113    #[cfg(feature = "blocking")]
114    fn test_fetch_with_invalid_token() {
115        // Test that invalid token returns Unauthorized
116        let result = fetch_usage_raw("invalid-token");
117        assert!(matches!(result, Err(ApiError::Unauthorized)));
118    }
119}