1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
use backon::Retryable;
use crate::{Error, traits::ClassifiableError};
use reqwest::header::HeaderMap;
// -------------------------------------------------------------------------------------------------
impl crate::Client {
/// Performs the HTTP POST request and returns the response to the caller.
///
/// ## Arguments
///
/// * `request` - This request will be converted into a URL query string, a (presumably JSON)
/// request body, and forwarded to Google Maps.
///
/// # Errors
///
/// This method can fail if:
///
/// * The request `struct` fails validation. For example, parameters in the request conflict
/// with one another, or the request parameters are set in a way that's incompatible.
///
/// For example, Google Maps Address Validation API will refuse a `PostalAddress` greater than
/// 280 characters. This will cause a validation failure.
///
/// * The HTTP client cannot make a connection to the Google Maps API server, or successfully
/// send the request or receive the response over the network.
///
/// * The Google Maps API server returns an unexpected response, or data in a format that's not
/// expected.
#[cfg_attr(feature = "tracing-instrumentation", tracing::instrument(
level = "debug",
skip(self, request),
fields(
endpoint = %REQ::title(),
),
err
))]
pub(crate) async fn post_request<REQ, RSP, ERR>(
&self,
request: REQ
) -> Result<RSP, Error>
where
REQ: std::fmt::Debug
+ crate::traits::EndPoint
+ crate::traits::QueryUrl
+ crate::traits::RequestBody
+ crate::traits::RequestHeaders
+ Send,
RSP: serde::de::DeserializeOwned + Into<Result<RSP, ERR>>,
ERR: std::fmt::Display + From<ERR> + Into<Error>
{
// Build the URL and query string
let url = request
.query_url()
.inspect_err(|error| tracing::error!(error = %error, "failed to build request URL"))?
.trim_matches('?')
.to_string();
tracing::info!(url = %url, "POST request");
// Build the request body
let body = request
.request_body()
.inspect_err(|error| tracing::error!(error = %error, "failed to build request body"))?;
// Get and set custom headers
let mut headers = request.request_headers(); // Request-specific headers
if REQ::send_x_goog_api_key() { // For requests that require API key in headers
let mut api_key = reqwest::header::HeaderValue::from_str(&self.key)
.map_err(|_error| Error::InvalidHeaderValue {
header_name: "X-Goog-Api-Key".to_string()
})?;
api_key.set_sensitive(true);
headers.insert("X-Goog-Api-Key", api_key);
}
// Observe any rate limiting before executing request:
self
.rate_limit
.limit_apis(REQ::apis())
.await;
// Build the HTTP request function with retry logic
let http_requestor = || async {
self.execute_post_request(&url, &body, &headers).await
};
// Perform the HTTP request with exponential backoff retry
let response = http_requestor
.retry(backon::ExponentialBuilder::default())
.when(|e: &Error| e.classify().is_transient())
.notify(|err, dur: std::time::Duration| {
tracing::warn!(
error = %err,
retry_after = ?dur,
"retrying request after error"
);
})
.await?;
tracing::debug!("request completed successfully");
Ok(response)
}
/// Executes a single POST request attempt.
///
/// Performs the HTTP POST, checks the response status, deserializes the response body, and
/// converts any API errors.
///
/// This is called by the retry logic in `post_request`.
#[tracing::instrument(
level = "trace",
skip(self, body, headers),
fields(
http.method = "POST",
http.url = %url,
http.headers_count = headers.len(),
),
err
)]
async fn execute_post_request<RSP, ERR>(
&self,
url: &str,
body: &str,
headers: &HeaderMap,
) -> Result<RSP, Error>
where
RSP: serde::de::DeserializeOwned + Into<Result<RSP, ERR>>,
ERR: std::fmt::Display + Into<Error>
{
// Build the request with custom headers
let request = self
.reqwest_client
.post(url)
.body(body.to_string())
.headers(headers.clone())
.build()
.inspect_err(|error| {
tracing::error!(
error = %error,
"failed to build HTTP request"
);
})
.map_err(|e| Error::from(crate::ReqError::from(e)))?;
// Execute the request
let response = self
.reqwest_client
.execute(request)
.await
.inspect_err(|error| tracing::error!(error = %error, "HTTP request failed"))
.map_err(Error::from)?;
// Check status
let status = response.status();
if !status.is_success() {
// Try to get the response body for error details
#[cfg(feature = "places-new-core")]
let error_body = response
.text()
.await
.unwrap_or_else(|_| "failed to read response body".to_string());
#[cfg(feature = "places-new-core")]
let google_api_error: crate::places_new::GoogleApiError =
serde_json::from_str(&error_body)?;
#[cfg(feature = "places-new-core")]
tracing::error!(
http.status_code = %status.as_u16(),
http.status_text = %status.canonical_reason().unwrap_or("unknown"),
response_body = %google_api_error,
"request returned non-success status"
);
#[cfg(feature = "places-new-core")]
return Err(Error::from(crate::places_new::Error::from(google_api_error)));
#[cfg(not(feature = "places-new-core"))]
return Err(Error::from(status));
}
tracing::trace!(http.status_code = %status.as_u16(), "received successful response");
// Get response body
let bytes = response
.text()
.await
.map(String::into_bytes)
.inspect_err(|error| tracing::error!(error = %error, "failed to read response body"))
.map_err(Error::from)?;
tracing::trace!(body_size = bytes.len(), "received response body");
// Deserialize JSON
let deserialized: RSP = serde_json::from_slice(&bytes)
.inspect_err(|error| {
tracing::error!(
error = %error,
"failed to deserialize JSON response"
);
if let Ok(text) = String::from_utf8(bytes.clone()) {
tracing::trace!(response_body = %text, "raw response body");
}
})
.inspect_err(|_| tracing::error!(
"error serializing from JSON:\n{}",
String::from_utf8(bytes).unwrap_or_default()))
.map_err(Error::from)?;
// Convert to final result (handles API-level errors)
let result: Result<RSP, ERR> = deserialized.into();
result
.inspect_err(|error| {
tracing::error!(
api_error = %error,
"API returned error response"
);
})
.map_err(Into::into)
}
}