reqwest_connect_rpc/
client.rs1use std::{borrow::Cow, sync::Arc, time::Duration};
17
18use anyhow::Context as _;
19use bytes::Bytes;
20use reqwest::header::{self, HeaderMap, HeaderValue};
21use thiserror::Error;
22use tracing::Instrument;
23
24use crate::{
25 error::CrpcError,
26 token_source::{TokenSource, TokenSourceError},
27};
28
29#[derive(Debug, Error)]
31pub enum CrpcClientError {
32 #[error("connection error {context}: {source:#?}")]
34 ConnectionError {
35 context: Cow<'static, str>,
37 source: Box<dyn std::error::Error + Send + Sync + 'static>,
39 },
40 #[error("server returned an error: {0:#?}")]
42 CrpcError(CrpcError),
43 #[error("failed to decode response body: {context}: {source:#?}")]
45 DecodeError {
46 context: Cow<'static, str>,
48 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
50 body: Option<Bytes>,
52 },
53 #[error("failed to retrieve token: {0}")]
55 TokenSourceError(#[from] TokenSourceError),
56}
57
58const APPLICATION_PROTO: &str = "application/proto";
59
60pub struct CrpcClient {
62 http_client: reqwest::Client,
63 base_url: url::Url,
64 token_source: Option<Arc<dyn TokenSource>>,
65 user_agent: HeaderValue,
66}
67
68impl CrpcClient {
69 pub fn new(base_url: &url::Url) -> anyhow::Result<Self> {
71 let http_client = reqwest::ClientBuilder::new()
72 .timeout(Duration::from_secs(30))
73 .build()
74 .context("error creating HTTP client")?;
75
76 Self::new_with_client(base_url, http_client)
77 }
78
79 pub fn new_with_client(
81 base_url: &url::Url,
82 http_client: reqwest::Client,
83 ) -> anyhow::Result<Self> {
84 let user_agent =
85 HeaderValue::from_str(&format!("reqwest-crpc {}", env!("CARGO_PKG_VERSION")))
86 .context("error creating user agent header")?;
87
88 Ok(CrpcClient {
89 http_client,
90 base_url: base_url.clone(),
91 token_source: None,
92 user_agent,
93 })
94 }
95
96 pub fn use_token_source(&mut self, token_source: Arc<dyn TokenSource>) -> &mut Self {
98 self.token_source = Some(token_source);
99 self
100 }
101
102 pub fn use_user_agent(&mut self, user_agent: &str) -> anyhow::Result<&mut Self> {
104 self.user_agent = HeaderValue::from_str(user_agent)
105 .with_context(|| format!("error creating user agent header from {user_agent}"))?;
106 Ok(self)
107 }
108
109 pub async fn unary_request<Req, Res>(
111 &self,
112 path: &str,
113 req: &Req,
114 ) -> Result<Res, CrpcClientError>
115 where
116 Req: prost::Message,
117 Res: prost::Message + Default,
118 {
119 self.do_unary_request(path, req)
120 .instrument(tracing::info_span!("request", %path, id = rand::random::<u16>()))
121 .await
122 }
123
124 async fn do_unary_request<Req, Res>(
126 &self,
127 path: &str,
128 req: &Req,
129 ) -> Result<Res, CrpcClientError>
130 where
131 Req: prost::Message,
132 Res: prost::Message + Default,
133 {
134 let url = self.base_url.join(path).map_err(|e| {
135 CrpcClientError::ConnectionError {
136 context: "error joining base URL and path".into(),
137 source: e.into(),
138 }
139 })?;
140
141 let mut headers = HeaderMap::with_capacity(3);
142 headers.insert(
143 header::CONTENT_TYPE,
144 header::HeaderValue::from_static(APPLICATION_PROTO),
145 );
146 headers.insert(header::USER_AGENT, self.user_agent.clone());
147
148 tracing::trace!(?url, ?headers, "Sending crpc unary request");
149
150 if let Some(token_source) = &self.token_source {
151 let token = token_source.get_token().await?;
152 let token_header = header::HeaderValue::from_str(&token_source.format_header(token))
153 .map_err(|e| {
154 CrpcClientError::TokenSourceError(
155 format!("error formatting token as header value: {e:?}").into(),
156 )
157 })?;
158
159 headers.insert(header::AUTHORIZATION, token_header);
160 }
161
162 let body = req.encode_to_vec();
163 let response = self
164 .http_client
165 .post(url)
166 .body(reqwest::Body::from(body))
167 .headers(headers)
168 .send()
169 .await
170 .map_err(|e| {
171 CrpcClientError::ConnectionError {
172 context: "error sending request".into(),
173 source: e.into(),
174 }
175 })?;
176
177 tracing::trace!(status=%response.status(), body_len=%response.content_length().unwrap_or(0), "Received crpc unary response");
178
179 let status = response.status();
180 if !status.is_success() {
181 let response_raw = response
182 .text()
183 .await
184 .unwrap_or_else(|_| "<failed to read body>".to_string());
185
186 match serde_json::from_str::<CrpcError>(&response_raw) {
188 Ok(crpc_err) => {
189 return Err(CrpcClientError::CrpcError(crpc_err));
190 }
191 Err(_) => {
192 return Err(CrpcClientError::CrpcError(CrpcError::new(
193 status.into(),
194 response_raw,
195 )));
196 }
197 }
198 }
199
200 let body = response.bytes().await.map_err(|e| {
201 CrpcClientError::DecodeError {
202 context: "error reading response body".into(),
203 source: Some(e.into()),
204 body: None,
205 }
206 })?;
207
208 Res::decode(&body[..]).map_err(|e| {
209 CrpcClientError::DecodeError {
210 context: "error decoding response body".into(),
211 source: Some(e.into()),
212 body: Some(body.clone()),
213 }
214 })
215 }
216}