statsig_client/
response.rs1use 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
15pub struct ApiResponseHandler;
17
18impl ApiResponseHandler {
19 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 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 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}