aliyun_oss/http/
client.rs1use async_trait::async_trait;
4use http::HeaderMap;
5
6use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
7
8#[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 pub fn builder() -> HttpRequestBuilder {
20 HttpRequestBuilder::default()
21 }
22}
23
24#[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 pub fn method(mut self, method: http::Method) -> Self {
36 self.method = Some(method);
37 self
38 }
39
40 pub fn uri(mut self, uri: impl Into<String>) -> Self {
42 self.uri = Some(uri.into());
43 self
44 }
45
46 pub fn header(mut self, key: http::HeaderName, value: http::HeaderValue) -> Self {
48 self.headers.insert(key, value);
49 self
50 }
51
52 pub fn body(mut self, body: impl Into<bytes::Bytes>) -> Self {
54 self.body = Some(body.into());
55 self
56 }
57
58 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#[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 pub fn new(status: http::StatusCode) -> Self {
80 Self {
81 status,
82 headers: HeaderMap::new(),
83 body: bytes::Bytes::new(),
84 }
85 }
86
87 pub fn status(&self) -> http::StatusCode {
89 self.status
90 }
91
92 pub fn is_success(&self) -> bool {
94 self.status.is_success()
95 }
96
97 pub fn body_as_str(&self) -> Option<&str> {
99 std::str::from_utf8(&self.body).ok()
100 }
101}
102
103#[async_trait]
105pub trait HttpClient: Send + Sync {
106 async fn send(&self, request: HttpRequest) -> Result<HttpResponse>;
108}
109
110pub struct ReqwestHttpClient {
112 inner: reqwest::Client,
113}
114
115impl ReqwestHttpClient {
116 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}