1use crate::wasip3::http::{client, types};
22use crate::wasip3::http_compat::{BodyWriter, http_from_wasi_response};
23use bytes::Bytes;
24use http_body::Body;
25use http_body_util::{BodyExt, Full};
26use std::future::IntoFuture;
27
28pub use crate::wasip3::http_compat::{IncomingResponseBody, RequestOptionsExtension, Response};
29pub use ::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, Uri, header};
30
31#[derive(Debug, thiserror::Error)]
33pub enum Error {
34 #[error("WASI HTTP request or response body failed: {0}")]
35 Wasi(#[from] types::ErrorCode),
36 #[error("invalid request headers: {0}")]
37 Headers(#[from] types::HeaderError),
38 #[error("request body upload failed: {0}")]
39 Upload(#[from] crate::wasip3::http_compat::Error),
40 #[error("response body is not UTF-8: {0}")]
41 Utf8(#[from] std::string::FromUtf8Error),
42 #[cfg(feature = "json")]
43 #[error("response body is not valid JSON: {0}")]
44 Json(#[from] serde_json::Error),
45}
46
47pub async fn send(request: Request<Vec<u8>>) -> Result<Response, Error> {
54 let (parts, body) = request.into_parts();
55 let options = parts
56 .extensions
57 .get::<RequestOptionsExtension>()
58 .cloned()
59 .map(|options| options.0);
60 let headers = types::Fields::try_from(parts.headers)?;
61 let (writer, contents, trailers) = BodyWriter::new();
62 let (request, transmitted) = types::Request::new(headers, Some(contents), trailers, options);
63
64 request
65 .set_method(&parts.method.into())
66 .map_err(|()| types::ErrorCode::HttpRequestMethodInvalid)?;
67 let scheme = parts.uri.scheme().map(Into::into);
68 request
69 .set_scheme(scheme.as_ref())
70 .map_err(|()| types::ErrorCode::HttpProtocolError)?;
71 request
72 .set_authority(parts.uri.authority().map(|authority| authority.as_str()))
73 .map_err(|()| types::ErrorCode::HttpRequestUriInvalid)?;
74 request
75 .set_path_with_query(parts.uri.path_and_query().map(|path| path.as_str()))
76 .map_err(|()| types::ErrorCode::HttpRequestUriInvalid)?;
77
78 let mut body = Full::new(Bytes::from(body));
79 let (response, transmitted, uploaded) = futures::join!(
82 client::send(request),
83 transmitted.into_future(),
84 writer.send_http_body(&mut body),
85 );
86 uploaded?;
87 transmitted?;
88 Ok(http_from_wasi_response(response?)?)
89}
90
91pub async fn response_bytes(
94 response: Response<impl Body<Data = Bytes, Error = types::ErrorCode>>,
95) -> Result<Bytes, Error> {
96 Ok(response.into_body().collect().await?.to_bytes())
97}
98
99pub async fn response_text(
101 response: Response<impl Body<Data = Bytes, Error = types::ErrorCode>>,
102) -> Result<String, Error> {
103 Ok(String::from_utf8(response_bytes(response).await?.to_vec())?)
104}
105
106#[cfg(feature = "json")]
108pub async fn response_json<T: serde::de::DeserializeOwned>(
109 response: Response<impl Body<Data = Bytes, Error = types::ErrorCode>>,
110) -> Result<T, Error> {
111 Ok(serde_json::from_slice(&response_bytes(response).await?)?)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use futures::stream;
118 use http_body::Frame;
119 use http_body_util::StreamBody;
120 use test_r::test;
121
122 fn response(
123 frames: Vec<Result<Frame<Bytes>, types::ErrorCode>>,
124 ) -> Response<impl Body<Data = Bytes, Error = types::ErrorCode>> {
125 Response::builder()
126 .status(StatusCode::BAD_REQUEST)
127 .body(StreamBody::new(stream::iter(frames)))
128 .unwrap()
129 }
130
131 #[test]
132 async fn collects_body_through_trailers_and_reports_late_errors() {
133 let mut trailers = HeaderMap::new();
134 trailers.insert("x-checksum", HeaderValue::from_static("ok"));
135 let frames = vec![
136 Ok(Frame::data(Bytes::from_static(b"abc"))),
137 Ok(Frame::data(Bytes::from_static(b"defgh"))),
138 Ok(Frame::trailers(trailers)),
139 ];
140 assert_eq!(response_bytes(response(frames)).await.unwrap(), "abcdefgh");
141
142 let frames = vec![
143 Ok(Frame::data(Bytes::from_static(b"partial"))),
144 Err(types::ErrorCode::ConnectionTerminated),
145 ];
146 assert!(matches!(
147 response_bytes(response(frames)).await,
148 Err(Error::Wasi(types::ErrorCode::ConnectionTerminated))
149 ));
150 }
151
152 #[test]
153 async fn text_rejects_invalid_utf8() {
154 assert!(matches!(
155 response_text(response(vec![Ok(Frame::data(Bytes::from_static(b"\xff")))])).await,
156 Err(Error::Utf8(_))
157 ));
158 }
159
160 #[cfg(feature = "json")]
161 #[test]
162 async fn json_decodes_error_status_bodies_and_rejects_malformed_json() {
163 let frames = vec![
164 Ok(Frame::data(Bytes::from_static(b"{\"message\":"))),
165 Ok(Frame::data(Bytes::from_static(b"\"bad input\"}"))),
166 ];
167 let value: serde_json::Value = response_json(response(frames)).await.unwrap();
168 assert_eq!(value, serde_json::json!({ "message": "bad input" }));
169 assert!(matches!(
170 response_json::<serde_json::Value>(response(vec![Ok(Frame::data(
171 Bytes::from_static(b"not json")
172 ))]))
173 .await,
174 Err(Error::Json(_))
175 ));
176 }
177}