polyoxide_binance/usdm/
request.rs1use std::marker::PhantomData;
4
5use polyoxide_core::{retry_after_header, truncate_for_log, ApiError, HttpClient};
6use reqwest::{Response, StatusCode};
7use serde::de::DeserializeOwned;
8
9use crate::{
10 error::{retry_after_secs, BinanceError},
11 weight::{Cost, Route, WeightBudget, DEFAULT_BAN},
12};
13
14pub const USED_WEIGHT_HEADER: &str = "x-mbx-used-weight-1m";
16
17#[must_use = "a request does nothing until it is sent"]
19pub struct WeightedRequest<T> {
20 http: HttpClient,
21 budget: WeightBudget,
22 route: Route,
23 query: Vec<(&'static str, String)>,
24 _marker: PhantomData<fn() -> T>,
25}
26
27impl<T> WeightedRequest<T> {
28 pub(crate) fn new(http: &HttpClient, budget: &WeightBudget, route: Route) -> Self {
29 Self {
30 http: http.clone(),
31 budget: budget.clone(),
32 route,
33 query: Vec::new(),
34 _marker: PhantomData,
35 }
36 }
37
38 pub(crate) fn query(mut self, key: &'static str, value: impl ToString) -> Self {
40 let value = value.to_string();
41 match self.query.iter_mut().find(|(k, _)| *k == key) {
42 Some(slot) => slot.1 = value,
43 None => self.query.push((key, value)),
44 }
45 self
46 }
47
48 pub(crate) fn route(mut self, route: Route) -> Self {
50 self.route = route;
51 self
52 }
53
54 pub fn cost(&self) -> Cost {
56 self.route.cost()
57 }
58}
59
60impl<T: DeserializeOwned> WeightedRequest<T> {
61 pub async fn send(self) -> Result<T, BinanceError> {
72 let text = self.send_text().await?;
73 serde_json::from_str(&text).map_err(|err| {
74 tracing::error!(
75 "Failed to decode {}: {err}: {}",
76 self.route.path(),
77 truncate_for_log(&text)
78 );
79 BinanceError::from(ApiError::from(err))
80 })
81 }
82
83 async fn send_text(&self) -> Result<String, BinanceError> {
84 let path = self.route.path();
85 let url = self.http.base_url.join(path)?;
86 let cost = self.route.cost();
87 let mut attempt = 0u32;
88
89 loop {
90 let permit = self.http.acquire_concurrency().await;
95 let charge = self.budget.acquire(cost).await;
96
97 let mut request = self.http.client.get(url.clone());
98 if !self.query.is_empty() {
99 request = request.query(&self.query);
100 }
101 let response = request.send().await?;
102 let status = response.status();
103 let retry_after = retry_after_header(&response);
104
105 if let Some(used) = used_weight(&response) {
108 self.budget.record_used(charge, used);
109 }
110
111 if status == StatusCode::IM_A_TEAPOT {
112 let ban = retry_after_secs(retry_after.as_deref()).unwrap_or(DEFAULT_BAN);
113 self.budget.begin_cooldown(ban);
114 let body = response.text().await.unwrap_or_default();
117 tracing::warn!(
118 "418 on {path}: IP banned, every request held {ban:?}: {}",
119 truncate_for_log(&body)
120 );
121 return Err(BinanceError::from_response_parts(
122 status.as_u16(),
123 retry_after.as_deref(),
124 &body,
125 ));
126 }
127 if status == StatusCode::TOO_MANY_REQUESTS {
128 let retry = self
133 .http
134 .should_retry(status, attempt, retry_after.as_deref());
135 let asked = retry_after_secs(retry_after.as_deref());
136 let cooldown = match (retry, asked) {
137 (None, None) => self.budget.hold_until_next_minute(),
138 _ => {
139 let cooldown = retry.unwrap_or_default().max(asked.unwrap_or_default());
140 self.budget.begin_cooldown(cooldown);
141 cooldown
142 }
143 };
144 if retry.is_some() {
145 attempt += 1;
146 tracing::warn!("429 on {path}, retry {attempt} after {cooldown:?}");
147 drop(permit);
148 continue;
149 }
150 tracing::warn!("429 on {path}, no retry left: every request held {cooldown:?}");
151 }
152
153 if !status.is_success() {
154 let body = response.text().await.unwrap_or_default();
155 return Err(BinanceError::from_response_parts(
156 status.as_u16(),
157 retry_after.as_deref(),
158 &body,
159 ));
160 }
161 return Ok(response.text().await?);
162 }
163 }
164}
165
166fn used_weight(response: &Response) -> Option<u32> {
167 response
168 .headers()
169 .get(USED_WEIGHT_HEADER)?
170 .to_str()
171 .ok()?
172 .trim()
173 .parse()
174 .ok()
175}