Skip to main content

px_exchange_polymarket/
client.rs

1use metrics::histogram;
2use reqwest::Client;
3use serde::de::DeserializeOwned;
4use std::time::Instant;
5
6use crate::config::PolymarketConfig;
7use crate::error::PolymarketError;
8
9pub struct HttpClient {
10    client: Client,
11    gamma_url: String,
12    clob_url: String,
13    verbose: bool,
14}
15
16impl HttpClient {
17    pub fn new(config: &PolymarketConfig) -> Result<Self, PolymarketError> {
18        // px_core::http::tuned_client_builder() pre-applies the openpx-wide
19        // HTTP tunings (HTTP/2 stream window, TCP_NODELAY, pool sizing,
20        // keep-alive). Per-exchange overrides layer on top.
21        let client = px_core::http::tuned_client_builder()
22            .timeout(config.base.timeout)
23            .build()?;
24
25        Ok(Self {
26            client,
27            gamma_url: config.gamma_url.clone(),
28            clob_url: config.clob_url.clone(),
29            verbose: config.base.verbose,
30        })
31    }
32
33    pub async fn get_gamma<T: DeserializeOwned>(
34        &self,
35        endpoint: &str,
36    ) -> Result<T, PolymarketError> {
37        let url = format!("{}{}", self.gamma_url, endpoint);
38        self.get(&url).await
39    }
40
41    pub async fn get_clob<T: DeserializeOwned>(
42        &self,
43        endpoint: &str,
44    ) -> Result<T, PolymarketError> {
45        let url = format!("{}{}", self.clob_url, endpoint);
46        self.get(&url).await
47    }
48
49    pub async fn get_response(&self, url: &str) -> Result<reqwest::Response, PolymarketError> {
50        if self.verbose {
51            tracing::debug!("GET {}", url);
52        }
53
54        let send_start = Instant::now();
55        let response = self
56            .client
57            .get(url)
58            .send()
59            .await
60            .map_err(|e| PolymarketError::Network(e.to_string()))?;
61        let send_us = send_start.elapsed().as_secs_f64() * 1_000_000.0;
62        histogram!("openpx.exchange.http_send_us", "exchange" => "polymarket").record(send_us);
63
64        Ok(response)
65    }
66
67    async fn get<T: DeserializeOwned>(&self, url: &str) -> Result<T, PolymarketError> {
68        if self.verbose {
69            tracing::debug!("GET {}", url);
70        }
71
72        let send_start = Instant::now();
73        let response = self.client.get(url).send().await?;
74        let send_us = send_start.elapsed().as_secs_f64() * 1_000_000.0;
75        histogram!("openpx.exchange.http_send_us", "exchange" => "polymarket").record(send_us);
76        let status = response.status();
77        let headers = response.headers().clone();
78
79        if status == 429 {
80            let retry_after = headers
81                .get("retry-after")
82                .and_then(|h| h.to_str().ok())
83                .and_then(|s| s.parse().ok())
84                .unwrap_or(1);
85            return Err(PolymarketError::RateLimited { retry_after });
86        }
87
88        let body_start = Instant::now();
89        let body = response.text().await?;
90        let body_us = body_start.elapsed().as_secs_f64() * 1_000_000.0;
91        histogram!("openpx.exchange.http_body_us", "exchange" => "polymarket").record(body_us);
92
93        if !status.is_success() {
94            return Err(PolymarketError::Api(format!("{status}: {body}")));
95        }
96
97        let parse_start = Instant::now();
98        let parsed = serde_json::from_str(&body)
99            .map_err(|e| PolymarketError::InvalidResponse(format!("parse error: {e}")))?;
100        let parse_us = parse_start.elapsed().as_secs_f64() * 1_000_000.0;
101        histogram!("openpx.exchange.json_parse_us", "exchange" => "polymarket").record(parse_us);
102
103        Ok(parsed)
104    }
105}