Skip to main content

rustolio_utils/http/request/
mod.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11mod builder;
12
13#[cfg(not(target_arch = "wasm32"))]
14use bytes::Bytes;
15#[cfg(target_arch = "wasm32")]
16use js_sys::JsString;
17#[cfg(target_arch = "wasm32")]
18use wasm_bindgen::JsCast as _;
19#[cfg(target_arch = "wasm32")]
20use wasm_bindgen::JsValue;
21#[cfg(target_arch = "wasm32")]
22use web_sys::RequestInit;
23
24use crate::bytes::IntoUtf8Bytes;
25#[cfg(not(target_arch = "wasm32"))]
26use crate::threadsafe;
27
28use super::{
29    Error, HeaderName, HeaderValue, Incoming, Method, Outgoing, Response, Result, Uri, Version,
30};
31
32pub use builder::Builder;
33
34// actual request only needed on server-side. on client side only the builder is used
35pub struct Request<B>(
36    #[cfg(not(target_arch = "wasm32"))] http::Request<B>,
37    #[cfg(target_arch = "wasm32")] std::marker::PhantomData<B>,
38);
39
40impl Request<()> {
41    // Does not allow body
42    #[inline]
43    pub fn get(uri: impl IntoUtf8Bytes) -> Builder<Outgoing> {
44        Builder::<Outgoing>::new(Method::GET, uri.into_utf8_bytes())
45    }
46
47    // Must have body
48    #[inline]
49    pub fn post(uri: impl IntoUtf8Bytes) -> Builder<()> {
50        Builder::<()>::new(Method::POST, uri.into_utf8_bytes())
51    }
52
53    // Must have body
54    #[inline]
55    pub fn patch(uri: impl IntoUtf8Bytes) -> Builder<()> {
56        Builder::<()>::new(Method::PATCH, uri.into_utf8_bytes())
57    }
58
59    // Must have body
60    #[inline]
61    pub fn put(uri: impl IntoUtf8Bytes) -> Builder<()> {
62        Builder::<()>::new(Method::PUT, uri.into_utf8_bytes())
63    }
64
65    // Must have body
66    #[inline]
67    pub fn delete(uri: impl IntoUtf8Bytes) -> Builder<()> {
68        Builder::<()>::new(Method::DELETE, uri.into_utf8_bytes())
69    }
70}
71
72impl<B> Request<B> {
73    #[cfg(not(target_arch = "wasm32"))]
74    pub fn method(&self) -> &Method {
75        self.0.method()
76    }
77
78    #[cfg(not(target_arch = "wasm32"))]
79    pub fn header(&self, key: impl IntoUtf8Bytes) -> Option<&HeaderValue> {
80        let key = key.into();
81        let key: http::HeaderName = key.to_vec().try_into().ok()?;
82        self.0.headers().get(&key)
83    }
84
85    #[cfg(not(target_arch = "wasm32"))]
86    pub fn uri(&self) -> &Uri {
87        self.0.uri()
88    }
89
90    #[cfg(not(target_arch = "wasm32"))]
91    pub fn body(&self) -> &B {
92        self.0.body()
93    }
94
95    #[cfg(not(target_arch = "wasm32"))]
96    pub fn into_body(self) -> B {
97        self.0.into_body()
98    }
99}
100
101impl Request<Incoming> {
102    #[cfg(not(target_arch = "wasm32"))]
103    pub fn from_inner(req: http::Request<Incoming>) -> Self {
104        Self(req)
105    }
106}
107
108#[cfg(not(target_arch = "wasm32"))]
109impl<B> Request<B>
110where
111    B: hyper::body::Body<Error: threadsafe::Error>,
112{
113    pub async fn text(self) -> Result<Request<String>> {
114        use http_body_util::BodyExt;
115
116        let (parts, body) = self.0.into_parts();
117
118        let body = body.collect().await.map_err(Error::body)?;
119        let Ok(text) = String::from_utf8(body.to_bytes().to_vec()) else {
120            return Err(Error::InvalidType);
121        };
122
123        Ok(Request(http::Request::from_parts(parts, text)))
124    }
125
126    pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<Request<T>> {
127        use http_body_util::BodyExt;
128
129        let Some(ty) = self.header(HeaderName::CONTENT_TYPE) else {
130            return Err(Error::InvalidType);
131        };
132        if !ty
133            .to_str()
134            .map_err(|_| Error::InvalidType)?
135            .starts_with("application/json")
136        {
137            return Err(Error::InvalidType);
138        }
139
140        let (parts, body) = self.0.into_parts();
141
142        let body = body.collect().await.map_err(Error::body)?;
143        let Ok(json) = serde_json::from_slice(&body.to_bytes()) else {
144            return Err(Error::InvalidType);
145        };
146
147        Ok(Request(http::Request::from_parts(parts, json)))
148    }
149
150    // TODO: Decode using the stream instead of collecting all bytes first
151    pub async fn encoded<T: crate::prelude::Decode>(self) -> Result<Request<T>> {
152        self.bytes().await.and_then(|b| {
153            let (parts, body) = b.0.into_parts();
154
155            let decoded: T =
156                crate::bytes::encoding::decode_from_bytes(body).map_err(Error::body)?;
157
158            Ok(Request(http::Request::from_parts(parts, decoded)))
159        })
160    }
161
162    pub async fn bytes(self) -> Result<Request<Bytes>> {
163        use http_body_util::BodyExt;
164
165        let (parts, body) = self.0.into_parts();
166
167        let body = body.collect().await.map_err(Error::body)?;
168        let bytes = body.to_bytes();
169
170        Ok(Request(http::Request::from_parts(parts, bytes)))
171    }
172}