Skip to main content

aliyun_oss/http/
client.rs

1//! HTTP client abstraction and Reqwest-based implementation.
2
3use async_trait::async_trait;
4use http::HeaderMap;
5
6use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
7
8/// A fully-formed HTTP request ready to be sent.
9#[derive(Debug)]
10pub struct HttpRequest {
11    pub method: http::Method,
12    pub uri: String,
13    pub headers: http::HeaderMap,
14    pub body: Option<bytes::Bytes>,
15}
16
17impl HttpRequest {
18    /// Creates a new `HttpRequestBuilder`.
19    pub fn builder() -> HttpRequestBuilder {
20        HttpRequestBuilder::default()
21    }
22}
23
24/// Builder for constructing an `HttpRequest`.
25#[derive(Default)]
26pub struct HttpRequestBuilder {
27    method: Option<http::Method>,
28    uri: Option<String>,
29    headers: http::HeaderMap,
30    body: Option<bytes::Bytes>,
31}
32
33impl HttpRequestBuilder {
34    /// Sets the HTTP method.
35    pub fn method(mut self, method: http::Method) -> Self {
36        self.method = Some(method);
37        self
38    }
39
40    /// Sets the request URI.
41    pub fn uri(mut self, uri: impl Into<String>) -> Self {
42        self.uri = Some(uri.into());
43        self
44    }
45
46    /// Adds a header to the request.
47    pub fn header(mut self, key: http::HeaderName, value: http::HeaderValue) -> Self {
48        self.headers.insert(key, value);
49        self
50    }
51
52    /// Sets the request body.
53    pub fn body(mut self, body: impl Into<bytes::Bytes>) -> Self {
54        self.body = Some(body.into());
55        self
56    }
57
58    /// Builds the `HttpRequest`.
59    pub fn build(self) -> HttpRequest {
60        HttpRequest {
61            method: self.method.unwrap_or(http::Method::GET),
62            uri: self.uri.unwrap_or_default(),
63            headers: self.headers,
64            body: self.body,
65        }
66    }
67}
68
69/// An HTTP response containing status, headers, and body.
70#[derive(Debug)]
71pub struct HttpResponse {
72    pub status: http::StatusCode,
73    pub headers: http::HeaderMap,
74    pub body: bytes::Bytes,
75}
76
77impl HttpResponse {
78    /// Creates a new `HttpResponse` with the given status code.
79    pub fn new(status: http::StatusCode) -> Self {
80        Self {
81            status,
82            headers: HeaderMap::new(),
83            body: bytes::Bytes::new(),
84        }
85    }
86
87    /// Returns the HTTP status code.
88    pub fn status(&self) -> http::StatusCode {
89        self.status
90    }
91
92    /// Returns `true` if the status is in the 2xx range.
93    pub fn is_success(&self) -> bool {
94        self.status.is_success()
95    }
96
97    /// Returns the response body as a UTF-8 string, if valid.
98    pub fn body_as_str(&self) -> Option<&str> {
99        std::str::from_utf8(&self.body).ok()
100    }
101}
102
103/// Trait abstracting the HTTP transport layer.
104#[async_trait]
105pub trait HttpClient: Send + Sync {
106    /// Sends an HTTP request and returns the response.
107    async fn send(&self, request: HttpRequest) -> Result<HttpResponse>;
108}
109
110/// Reqwest-based implementation of `HttpClient`.
111pub struct ReqwestHttpClient {
112    inner: reqwest::Client,
113}
114
115impl ReqwestHttpClient {
116    /// Creates a new `ReqwestHttpClient`.
117    pub fn new() -> Result<Self> {
118        let client = reqwest::Client::builder().build().map_err(|e| OssError {
119            kind: OssErrorKind::ConfigError,
120            context: Box::new(ErrorContext {
121                operation: Some("create ReqwestHttpClient".into()),
122                ..Default::default()
123            }),
124            source: Some(Box::new(e)),
125        })?;
126        Ok(Self { inner: client })
127    }
128}
129
130impl Default for ReqwestHttpClient {
131    fn default() -> Self {
132        Self::new().expect("create default ReqwestHttpClient")
133    }
134}
135
136#[async_trait]
137impl HttpClient for ReqwestHttpClient {
138    async fn send(&self, request: HttpRequest) -> Result<HttpResponse> {
139        let mut req = self.inner.request(request.method, &request.uri);
140
141        for (name, value) in request.headers.iter() {
142            req = req.header(name, value);
143        }
144
145        if let Some(body) = request.body {
146            req = req.body(body);
147        }
148
149        let response = req.send().await.map_err(|e| OssError {
150            kind: OssErrorKind::TransportError,
151            context: Box::new(ErrorContext {
152                operation: Some("send HTTP request".into()),
153                ..Default::default()
154            }),
155            source: Some(Box::new(e)),
156        })?;
157
158        let status = response.status();
159        let headers = response.headers().clone();
160        let body = response.bytes().await.map_err(|e| OssError {
161            kind: OssErrorKind::TransportError,
162            context: Box::new(ErrorContext {
163                operation: Some("read HTTP response body".into()),
164                ..Default::default()
165            }),
166            source: Some(Box::new(e)),
167        })?;
168
169        Ok(HttpResponse {
170            status,
171            headers,
172            body,
173        })
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn http_request_builder_sets_method_and_uri() {
183        let request = HttpRequest::builder()
184            .method(http::Method::PUT)
185            .uri("https://oss-cn-hangzhou.aliyuncs.com/bucket/key")
186            .build();
187        assert_eq!(request.method, http::Method::PUT);
188        assert_eq!(
189            request.uri,
190            "https://oss-cn-hangzhou.aliyuncs.com/bucket/key"
191        );
192    }
193
194    #[test]
195    fn http_request_builder_sets_headers() {
196        let request = HttpRequest::builder()
197            .method(http::Method::GET)
198            .uri("https://example.com")
199            .header(
200                http::HeaderName::from_static("content-type"),
201                http::HeaderValue::from_static("text/plain"),
202            )
203            .build();
204        assert_eq!(
205            request
206                .headers
207                .get("content-type")
208                .unwrap()
209                .to_str()
210                .unwrap(),
211            "text/plain"
212        );
213    }
214
215    #[test]
216    fn http_request_builder_sets_body() {
217        let body = bytes::Bytes::from_static(b"hello world");
218        let request = HttpRequest::builder()
219            .method(http::Method::POST)
220            .uri("https://example.com")
221            .body(body.clone())
222            .build();
223        assert_eq!(request.body.as_deref(), Some(b"hello world" as &[u8]));
224    }
225
226    #[test]
227    fn http_request_builder_defaults() {
228        let request = HttpRequest::builder().build();
229        assert_eq!(request.method, http::Method::GET);
230        assert!(request.body.is_none());
231    }
232
233    #[test]
234    fn http_response_defaults() {
235        let response = HttpResponse::new(http::StatusCode::OK);
236        assert_eq!(response.status(), http::StatusCode::OK);
237        assert!(response.is_success());
238        assert!(response.body.is_empty());
239    }
240
241    #[test]
242    fn http_response_not_success() {
243        let response = HttpResponse::new(http::StatusCode::NOT_FOUND);
244        assert!(!response.is_success());
245    }
246
247    #[test]
248    fn http_client_trait_object_safe() {
249        fn _use_client(_client: &dyn HttpClient) {}
250    }
251
252    #[tokio::test]
253    async fn reqwest_client_send_get_request() {
254        let client = ReqwestHttpClient::new().unwrap();
255        let request = HttpRequest::builder()
256            .method(http::Method::GET)
257            .uri("https://httpbin.org/get")
258            .build();
259        let response = client.send(request).await.unwrap();
260        assert!(response.is_success());
261        assert_eq!(response.status(), http::StatusCode::OK);
262    }
263
264    #[test]
265    fn http_request_send_sync() {
266        fn assert_send_sync<T: Send + Sync>() {}
267        assert_send_sync::<HttpRequest>();
268        assert_send_sync::<HttpResponse>();
269    }
270}