open_payments/client/request.rs
1//! # HTTP Request Building and Execution
2//!
3//! This module provides the internal HTTP request building and execution functionality
4//! for the Open Payments client. It handles both authenticated and unauthenticated
5//! requests, including HTTP message signature creation and content digest generation.
6//!
7//! ## Key Components
8//!
9//! - Generic HTTP request builder for constructing requests
10//! - Request builders for authenticated and unauthenticated operations
11//! - Internal request execution and signature handling
12//!
13//! ## Features
14//!
15//! - **Automatic Signature Creation**: Authenticated requests automatically include HTTP message signatures
16//! - **Content Digest Generation**: SHA-512 content digests for request bodies
17//! - **GNAP Authorization**: Support for Grant Negotiation and Authorization Protocol tokens
18//! - **Error Handling**: Comprehensive error handling for HTTP and signature operations
19use crate::client::AuthenticatedOpenPaymentsClient;
20use crate::client::BaseClient;
21use crate::http_signature::{create_signature_headers, SignOptions};
22use crate::OpClientError;
23use crate::Result;
24use base64::engine::general_purpose;
25use base64::Engine;
26use http::{
27 header::{HeaderName, HeaderValue},
28 Method as HttpMethod, Request,
29};
30use reqwest::{Client, Method};
31use serde::de::DeserializeOwned;
32use sha2::{Digest, Sha512};
33
34/// Generic HTTP request builder for Open Payments operations.
35///
36/// This struct provides a fluent interface for building HTTP requests with
37/// optional body content. It's used internally by the client to construct
38/// requests before execution.
39///
40/// ## Type Parameters
41///
42/// - `C` - The client type (authenticated or unauthenticated)
43pub(crate) struct HttpRequest<'a, C> {
44 /// Reference to the client that will execute this request.
45 client: &'a C,
46 /// HTTP method for the request.
47 method: Method,
48 /// Target URL for the request.
49 url: String,
50 /// Optional request body content.
51 body: Option<String>,
52}
53
54impl<'a, C> HttpRequest<'a, C> {
55 /// Creates a new HTTP request builder.
56 ///
57 /// ## Arguments
58 ///
59 /// * `client` - Reference to the client that will execute the request
60 /// * `method` - HTTP method (GET, POST, PUT, DELETE, etc.)
61 /// * `url` - Target URL for the request
62 ///
63 /// ## Returns
64 ///
65 /// Returns a new `HttpRequest` builder with no body content.
66 pub fn new(client: &'a C, method: Method, url: String) -> Self {
67 Self {
68 client,
69 method,
70 url,
71 body: None,
72 }
73 }
74
75 /// Adds a body to the request.
76 ///
77 /// This method consumes the request builder and returns a new one with
78 /// the specified body content. The body is typically JSON for Open Payments
79 /// API requests.
80 ///
81 /// ## Arguments
82 ///
83 /// * `body` - The request body content as a string
84 ///
85 /// ## Returns
86 ///
87 /// Returns a new `HttpRequest` with the body content added.
88 pub fn with_body(mut self, body: String) -> Self {
89 self.body = Some(body);
90 self
91 }
92}
93
94/// Type alias for authenticated HTTP requests.
95///
96/// This type represents HTTP requests that will be executed with authentication,
97/// including HTTP message signatures and optional GNAP access tokens.
98pub(crate) type AuthenticatedRequest<'a> = HttpRequest<'a, AuthenticatedOpenPaymentsClient>;
99
100/// Type alias for unauthenticated HTTP requests.
101///
102/// This type represents HTTP requests that will be executed without authentication,
103/// suitable for public endpoints that don't require signatures or tokens.
104pub(crate) type UnauthenticatedRequest<'a> = HttpRequest<'a, Client>;
105
106impl AuthenticatedRequest<'_> {
107 /// Builds and executes an authenticated HTTP request.
108 ///
109 /// This method performs the following steps:
110 /// 1. Builds the HTTP request with proper headers
111 /// 2. Adds GNAP authorization header if a token is provided
112 /// 3. Generates content digest and length headers for request bodies
113 /// 4. Creates HTTP message signatures using the client's signing key
114 /// 5. Executes the request and deserializes the response
115 ///
116 /// ## Arguments
117 ///
118 /// * `access_token` - Optional GNAP access token for authorization
119 ///
120 /// ## Returns
121 ///
122 /// Returns the deserialized response of type `T`, or an error if the request fails.
123 ///
124 /// ## Errors
125 ///
126 /// Returns an `OpClientError` with:
127 /// - `description`: Human-readable error message
128 /// - `status`: HTTP status text (for HTTP errors)
129 /// - `code`: HTTP status code (for HTTP errors)
130 /// - `validation_errors`: List of validation errors (if applicable)
131 /// - `details`: Additional error details (if applicable)
132 pub async fn build_and_execute<T: DeserializeOwned + 'static>(
133 self,
134 access_token: Option<&str>,
135 ) -> Result<T> {
136 let mut req = build_request(&self)?;
137
138 if let Some(token) = access_token {
139 req.headers_mut().insert(
140 "Authorization",
141 format!("GNAP {token}").parse().map_err(|e| {
142 OpClientError::header_parse(format!(
143 "Failed to parse authorization header: {e}"
144 ))
145 })?,
146 );
147 }
148
149 if let Some((content_length, content_digest)) = Self::create_content_headers(&self.body) {
150 req.headers_mut().insert(
151 "Content-Length",
152 content_length.to_string().parse().map_err(|e| {
153 OpClientError::header_parse(format!("Failed to parse content length: {e}"))
154 })?,
155 );
156 req.headers_mut().insert(
157 "Content-Digest",
158 content_digest.parse().map_err(|e| {
159 OpClientError::header_parse(format!("Failed to parse content digest: {e}"))
160 })?,
161 );
162 }
163
164 let (signature, signature_input) = Self::create_signature_headers(&self, &req)?;
165
166 req.headers_mut().insert(
167 "Signature",
168 signature.parse().map_err(|e| {
169 OpClientError::header_parse(format!("Failed to parse signature header: {e}"))
170 })?,
171 );
172 req.headers_mut().insert(
173 "Signature-Input",
174 signature_input.parse().map_err(|e| {
175 OpClientError::header_parse(format!("Failed to parse signature input header: {e}"))
176 })?,
177 );
178
179 execute_request(&self.client.http_client, req).await
180 }
181
182 /// Creates HTTP message signature headers for the request.
183 ///
184 /// This method converts the reqwest request to an http::Request for signature
185 /// creation, then generates the signature and signature-input headers using
186 /// the client's signing key.
187 ///
188 /// ## Arguments
189 ///
190 /// * `req` - The reqwest request to sign
191 ///
192 /// ## Returns
193 ///
194 /// Returns a tuple of `(signature, signature_input)` strings, or an error if
195 /// signature creation fails.
196 fn create_signature_headers(&self, req: &reqwest::Request) -> Result<(String, String)> {
197 // Convert to http::Request for signing
198 let mut http_req = Request::new(self.body.clone());
199 *http_req.method_mut() = HttpMethod::from_bytes(req.method().as_str().as_bytes())
200 .map_err(|e| OpClientError::header_parse(format!("Converting HTTP method: {e}")))?;
201 *http_req.uri_mut() = req
202 .url()
203 .as_str()
204 .parse()
205 .map_err(|e| OpClientError::header_parse(format!("Converting URL to URI: {e}")))?;
206
207 for (key, value) in req.headers() {
208 let header_name = HeaderName::from_bytes(key.as_str().as_bytes())
209 .map_err(|e| OpClientError::header_parse(format!("Converting header name: {e}")))?;
210 let header_value = HeaderValue::from_bytes(value.as_bytes()).map_err(|e| {
211 OpClientError::header_parse(format!("Converting header value: {e}"))
212 })?;
213 http_req.headers_mut().insert(header_name, header_value);
214 }
215
216 // Create and return signature headers
217 let options = SignOptions::new(
218 &http_req,
219 &self.client.signing_key,
220 self.client.config.key_id.clone(),
221 );
222 let headers = create_signature_headers(options)
223 .map_err(|e| OpClientError::signature(e.to_string()))?;
224
225 Ok((headers.signature, headers.signature_input))
226 }
227
228 /// Creates content headers for request bodies.
229 ///
230 /// This method generates `Content-Length` and `Content-Digest` headers for
231 /// requests with body content. The content digest uses SHA-512 hashing
232 /// as required by the Open Payments specification.
233 ///
234 /// ## Arguments
235 ///
236 /// * `body` - Optional request body content
237 ///
238 /// ## Returns
239 ///
240 /// Returns `Some((content_length, content_digest))` if a body is present,
241 /// or `None` if no body content.
242 fn create_content_headers(body: &Option<String>) -> Option<(usize, String)> {
243 match body {
244 Some(body) => {
245 let content_length = body.len();
246 let mut hasher = Sha512::new();
247 hasher.update(body.as_bytes());
248 let digest = general_purpose::STANDARD.encode(hasher.finalize());
249 let content_digest = format!("sha-512=:{digest}:");
250
251 Some((content_length, content_digest))
252 }
253 None => None,
254 }
255 }
256}
257
258impl UnauthenticatedRequest<'_> {
259 /// Builds and executes an unauthenticated HTTP request.
260 ///
261 /// This method builds and executes a request without authentication headers
262 /// or signatures. It's suitable for public endpoints that don't require
263 /// authentication.
264 ///
265 /// ## Returns
266 ///
267 /// Returns the deserialized response of type `T`, or an error if the request fails.
268 ///
269 /// ## Errors
270 ///
271 /// Returns an `OpClientError` with:
272 /// - `description`: Human-readable error message
273 /// - `status`: HTTP status text (for HTTP errors)
274 /// - `code`: HTTP status code (for HTTP errors)
275 /// - `validation_errors`: List of validation errors (if applicable)
276 /// - `details`: Additional error details (if applicable)
277 pub async fn build_and_execute<T: DeserializeOwned + 'static>(self) -> Result<T> {
278 let req = build_request(&self)?;
279 execute_request(self.client, req).await
280 }
281}
282
283impl<C: BaseClient> BaseClient for HttpRequest<'_, C> {
284 fn http_client(&self) -> &reqwest::Client {
285 self.client.http_client()
286 }
287}
288
289/// Builds a reqwest request from the HTTP request builder.
290///
291/// This function creates a reqwest request with the appropriate method, URL,
292/// and body content. It also sets the `Content-Type` header to `application/json`.
293///
294/// ## Arguments
295///
296/// * `req` - The HTTP request builder
297///
298/// ## Returns
299///
300/// Returns a built reqwest request, or an error if the request cannot be built.
301fn build_request<C: BaseClient>(req: &HttpRequest<C>) -> Result<reqwest::Request> {
302 let mut builder = req
303 .http_client()
304 .request(req.method.clone(), &req.url)
305 .header("Content-Type", "application/json");
306
307 if let Some(body) = &req.body {
308 builder = builder.body(body.clone());
309 }
310
311 builder
312 .build()
313 .map_err(|e| Box::new(OpClientError::from(e)))
314}
315
316/// Executes a reqwest request and deserializes the response.
317///
318/// This function handles the HTTP request execution, status code checking,
319/// and response deserialization. It includes special handling for 204 No Content
320/// responses.
321///
322/// ## Arguments
323///
324/// * `client` - The reqwest client to use for execution
325/// * `req` - The reqwest request to execute
326///
327/// ## Returns
328///
329/// Returns the deserialized response of type `T`, or an error if the request fails.
330///
331/// ## Errors
332///
333/// Returns an `OpClientError` with:
334/// - `description`: Human-readable error message
335/// - `status`: HTTP status text (for HTTP errors)
336/// - `code`: HTTP status code (for HTTP errors)
337/// - `validation_errors`: List of validation errors (if applicable)
338/// - `details`: Additional error details (if applicable)
339async fn execute_request<T: DeserializeOwned + 'static>(
340 client: &Client,
341 req: reqwest::Request,
342) -> Result<T> {
343 let resp = client.execute(req).await.map_err(OpClientError::from)?;
344
345 if !resp.status().is_success() {
346 return Err(Box::new(OpClientError::http(
347 "HTTP request failed".to_string(),
348 Some(
349 resp.status()
350 .canonical_reason()
351 .unwrap_or("Unknown")
352 .to_string(),
353 ),
354 Some(resp.status().as_u16()),
355 )));
356 }
357
358 if resp.status() == reqwest::StatusCode::NO_CONTENT
359 && std::any::TypeId::of::<T>() == std::any::TypeId::of::<()>()
360 {
361 return Ok(serde_json::from_str::<T>("null")
362 .expect("Deserializing unit type from null should never fail"));
363 }
364
365 let result: T = resp.json().await.map_err(OpClientError::from)?;
366
367 Ok(result)
368}