Skip to main content

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}