1use std::{
18 pin::Pin,
19 task::{Context, Poll},
20 time::Duration,
21};
22
23use bytes::Bytes;
24use http_body::Frame;
25use url::Url;
26
27use crate::error::BuildRequestError;
28
29pub trait IntoUrl: sealed::Sealed {
35 fn into_url(self) -> Result<Url, BuildRequestError>;
37}
38
39impl IntoUrl for Url {
40 fn into_url(self) -> Result<Url, BuildRequestError> {
41 Ok(self)
42 }
43}
44
45impl IntoUrl for &str {
46 fn into_url(self) -> Result<Url, BuildRequestError> {
47 Url::parse(self).map_err(|e| {
48 BuildRequestError::InvalidUrl {
49 source: Box::new(e),
50 }
51 })
52 }
53}
54
55impl IntoUrl for String {
56 fn into_url(self) -> Result<Url, BuildRequestError> {
57 self.as_str().into_url()
58 }
59}
60
61impl IntoUrl for &String {
62 fn into_url(self) -> Result<Url, BuildRequestError> {
63 self.as_str().into_url()
64 }
65}
66
67mod sealed {
68 pub trait Sealed {}
69 impl Sealed for url::Url {}
70 impl Sealed for &str {}
71 impl Sealed for String {}
72 impl Sealed for &String {}
73}
74
75#[derive(Debug, Clone)]
82pub struct Request {
83 method: http::Method,
84 url: Url,
85 headers: http::HeaderMap,
86 body: Bytes,
87 timeout: Option<Duration>,
88}
89
90impl Request {
91 #[must_use]
94 pub fn builder() -> RequestBuilder {
95 RequestBuilder::new()
96 }
97
98 #[must_use]
100 pub fn get(url: impl IntoUrl) -> RequestBuilder {
101 RequestBuilder::new().method(http::Method::GET).url(url)
102 }
103
104 #[must_use]
106 pub fn post(url: impl IntoUrl) -> RequestBuilder {
107 RequestBuilder::new().method(http::Method::POST).url(url)
108 }
109
110 #[must_use]
112 pub fn method(&self) -> &http::Method {
113 &self.method
114 }
115
116 #[must_use]
118 pub fn url(&self) -> &Url {
119 &self.url
120 }
121
122 #[must_use]
124 pub fn headers(&self) -> &http::HeaderMap {
125 &self.headers
126 }
127
128 #[must_use]
130 pub fn body(&self) -> &Bytes {
131 &self.body
132 }
133
134 #[must_use]
136 pub fn request_timeout(&self) -> Option<Duration> {
137 self.timeout
138 }
139
140 pub(crate) fn to_http(&self) -> Result<http::Request<BufferedBody>, crate::Error> {
143 let mut builder = http::Request::builder()
144 .method(self.method.clone())
145 .uri(self.url.as_str());
146 if let Some(headers) = builder.headers_mut() {
147 headers.extend(self.headers.clone());
148 }
149 builder
150 .body(BufferedBody::new(self.body.clone()))
151 .map_err(|e| {
152 crate::Error::InvalidRequest {
153 reason: e.to_string().into(),
154 }
155 })
156 }
157}
158
159#[derive(Debug)]
165pub struct RequestBuilder {
166 method: http::Method,
167 url: Option<Url>,
168 headers: http::HeaderMap,
169 body: Bytes,
170 timeout: Option<Duration>,
171 error: Option<BuildRequestError>,
172}
173
174impl RequestBuilder {
175 fn new() -> Self {
176 RequestBuilder {
177 method: http::Method::GET,
178 url: None,
179 headers: http::HeaderMap::new(),
180 body: Bytes::new(),
181 timeout: None,
182 error: None,
183 }
184 }
185
186 #[must_use]
188 pub fn method(mut self, method: http::Method) -> Self {
189 self.method = method;
190 self
191 }
192
193 #[must_use]
195 pub fn url(mut self, url: impl IntoUrl) -> Self {
196 match url.into_url() {
197 Ok(url) => self.url = Some(url),
198 Err(e) => {
199 self.error.get_or_insert(e);
200 }
201 }
202 self
203 }
204
205 #[must_use]
207 pub fn header<K, V>(mut self, key: K, value: V) -> Self
208 where
209 K: TryInto<http::header::HeaderName>,
210 K::Error: Into<http::Error>,
211 V: TryInto<http::header::HeaderValue>,
212 V::Error: Into<http::Error>,
213 {
214 match (key.try_into(), value.try_into()) {
215 (Ok(key), Ok(value)) => {
216 self.headers.append(key, value);
217 }
218 (Err(e), _) => {
219 self.error.get_or_insert(BuildRequestError::InvalidHeader {
220 source: Box::new(e.into()),
221 });
222 }
223 (_, Err(e)) => {
224 self.error.get_or_insert(BuildRequestError::InvalidHeader {
225 source: Box::new(e.into()),
226 });
227 }
228 }
229 self
230 }
231
232 #[must_use]
235 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
236 self.body = body.into();
237 self
238 }
239
240 #[must_use]
246 pub fn request_timeout(mut self, timeout: Duration) -> Self {
247 self.timeout = Some(timeout);
248 self
249 }
250
251 pub fn build(self) -> Result<Request, BuildRequestError> {
253 if let Some(error) = self.error {
254 return Err(error);
255 }
256 let url = self.url.ok_or(BuildRequestError::MissingUrl)?;
257 if url.scheme() != "https" {
258 return Err(BuildRequestError::UnsupportedScheme {
259 scheme: url.scheme().to_string(),
260 });
261 }
262 if url.host_str().is_none() {
263 return Err(BuildRequestError::MissingHost);
264 }
265 if !url.username().is_empty() || url.password().is_some() {
269 return Err(BuildRequestError::UserinfoNotAllowed);
270 }
271 Ok(Request {
272 method: self.method,
273 url,
274 headers: self.headers,
275 body: self.body,
276 timeout: self.timeout,
277 })
278 }
279}
280
281pub(crate) struct BufferedBody {
283 data: Option<Bytes>,
284}
285
286impl BufferedBody {
287 fn new(data: Bytes) -> Self {
288 let data = if data.is_empty() { None } else { Some(data) };
289 BufferedBody { data }
290 }
291}
292
293impl http_body::Body for BufferedBody {
294 type Data = Bytes;
295 type Error = std::convert::Infallible;
296
297 fn poll_frame(
298 self: Pin<&mut Self>,
299 _cx: &mut Context<'_>,
300 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
301 Poll::Ready(self.get_mut().data.take().map(|data| Ok(Frame::data(data))))
302 }
303
304 fn is_end_stream(&self) -> bool {
305 self.data.is_none()
306 }
307
308 fn size_hint(&self) -> http_body::SizeHint {
309 let len = self.data.as_ref().map_or(0, Bytes::len) as u64;
310 http_body::SizeHint::with_exact(len)
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn get_builds_with_defaults() {
320 let req = Request::get("https://chat.example.org/rooms")
321 .build()
322 .unwrap();
323 assert_eq!(req.method(), http::Method::GET);
324 assert_eq!(req.url().as_str(), "https://chat.example.org/rooms");
325 assert!(req.request_timeout().is_none());
326 assert!(req.body().is_empty());
327 }
328
329 #[test]
330 fn non_https_scheme_is_rejected() {
331 let err = Request::get("http://chat.example.org/")
332 .build()
333 .unwrap_err();
334 assert!(matches!(
335 err,
336 BuildRequestError::UnsupportedScheme { scheme } if scheme == "http"
337 ));
338 }
339
340 #[test]
341 fn userinfo_is_rejected() {
342 for url in [
343 "https://user:secret@api.example.org/x",
344 "https://user@api.example.org/x",
345 ] {
346 let err = Request::get(url).build().unwrap_err();
347 assert!(
348 matches!(err, BuildRequestError::UserinfoNotAllowed),
349 "{url}"
350 );
351 }
352 }
353
354 #[test]
355 fn unparsable_url_is_reported_at_build() {
356 let err = Request::get("not a url").build().unwrap_err();
357 assert!(matches!(err, BuildRequestError::InvalidUrl { .. }));
358 }
359
360 #[test]
361 fn missing_url_is_reported_at_build() {
362 let err = Request::builder().build().unwrap_err();
363 assert!(matches!(err, BuildRequestError::MissingUrl));
364 }
365
366 #[test]
367 fn invalid_header_is_deferred_to_build() {
368 let err = Request::get("https://example.org/")
369 .header("bad header name", "value")
370 .build()
371 .unwrap_err();
372 assert!(matches!(err, BuildRequestError::InvalidHeader { .. }));
373 }
374
375 #[test]
376 fn to_http_uses_the_full_url_as_uri() {
377 let req = Request::post("https://example.org:8443/echo")
378 .header("content-type", "text/plain")
379 .body("hello")
380 .build()
381 .unwrap();
382 let http_req = req.to_http().unwrap();
383 assert_eq!(http_req.uri(), "https://example.org:8443/echo");
384 assert_eq!(
385 http_req.uri().authority().map(http::uri::Authority::as_str),
386 Some("example.org:8443")
387 );
388 assert_eq!(http_req.headers()["content-type"], "text/plain");
389 }
390}