statsig_client/
response.rs

1//! Unified API response handling for Statsig client
2//!
3//! This module provides consistent response handling across all API endpoints,
4//! with proper error mapping and response parsing.
5
6use crate::{
7    api::{ConfigEvaluationResult, GateEvaluationResult},
8    error::{Result, StatsigError},
9};
10use reqwest::Response;
11use reqwest::header::RETRY_AFTER;
12use serde::Deserialize;
13use serde::de::DeserializeOwned;
14
15/// Handles API responses with consistent error mapping and parsing
16pub struct ApiResponseHandler;
17
18impl ApiResponseHandler {
19    /// Handles a generic API response
20    pub async fn handle<T: DeserializeOwned>(response: Response) -> Result<T> {
21        let status = response.status();
22
23        if status.is_success() {
24            let body = response.text().await?;
25            Self::parse_json(&body)
26                .map_err(|e| e.with_context(&format!("Response body: {}", Self::truncate(&body))))
27        } else {
28            Err(Self::error_from_response(status, response).await)
29        }
30    }
31
32    /// Handles gate-specific API responses with custom parsing
33    pub async fn handle_gate_response(response: Response) -> Result<Vec<GateEvaluationResult>> {
34        let status = response.status();
35
36        if status.is_success() {
37            let body = response.text().await?;
38
39            #[derive(Deserialize)]
40            struct GateEvaluationResultWire {
41                #[serde(default)]
42                name: Option<String>,
43                value: bool,
44                #[serde(rename = "rule_id")]
45                rule_id: Option<String>,
46                #[serde(rename = "group_name")]
47                group_name: Option<String>,
48            }
49
50            let map: std::collections::HashMap<String, GateEvaluationResultWire> =
51                Self::parse_json(&body).map_err(|e| {
52                    e.with_context(&format!("Response body: {}", Self::truncate(&body)))
53                })?;
54
55            Ok(map
56                .into_iter()
57                .map(|(gate_name, wire)| GateEvaluationResult {
58                    name: wire.name.unwrap_or(gate_name),
59                    value: wire.value,
60                    rule_id: wire.rule_id,
61                    group_name: wire.group_name,
62                })
63                .collect())
64        } else {
65            Err(Self::error_from_response(status, response).await)
66        }
67    }
68
69    /// Handles config-specific API responses
70    pub async fn handle_config_response(response: Response) -> Result<ConfigEvaluationResult> {
71        Self::handle(response).await
72    }
73
74    fn parse_json<T: DeserializeOwned>(body: &str) -> Result<T> {
75        serde_json::from_str(body)
76            .map_err(|e| StatsigError::serialization(format!("Failed to parse response JSON: {e}")))
77    }
78
79    fn truncate(body: &str) -> String {
80        const LIMIT: usize = 2_000;
81        if body.len() <= LIMIT {
82            body.to_string()
83        } else {
84            format!("{}...(truncated)", &body[..LIMIT])
85        }
86    }
87
88    async fn error_from_response(status: reqwest::StatusCode, response: Response) -> StatsigError {
89        let headers = response.headers().clone();
90        let body = match response.text().await {
91            Ok(body) => body,
92            Err(err) => return StatsigError::from(err),
93        };
94
95        match status.as_u16() {
96            401 => StatsigError::Unauthorized,
97            429 => {
98                let retry_after_seconds = headers
99                    .get(RETRY_AFTER)
100                    .and_then(|v| v.to_str().ok())
101                    .and_then(|s| s.parse::<u64>().ok())
102                    .unwrap_or(60);
103                StatsigError::rate_limited(retry_after_seconds)
104            }
105            _ => StatsigError::api(status.as_u16(), Self::truncate(&body)),
106        }
107    }
108}