1use core::fmt::Display;
21use core::marker::PhantomData;
22use core::ops::{Deref, DerefMut};
23
24use std::io::{Read, Write};
25
26use anyhow::{anyhow, Context as _};
27use wasi::http::types::{OutgoingResponse, ResponseOutparam};
28use wasi::io::streams::{InputStream, OutputStream, StreamError};
29
30pub use http::{
31 header, method, response, uri, HeaderMap, HeaderName, HeaderValue, Method, Request, Response,
32 StatusCode, Uri,
33};
34pub use wasi::http::types::ErrorCode;
35
36pub type Result<T, E = ErrorCode> = core::result::Result<T, E>;
37
38pub type IncomingRequest = Request<IncomingBody>;
39
40impl crate::From<Method> for wasi::http::types::Method {
41 fn from(method: Method) -> Self {
42 match method.as_str() {
43 "GET" => Self::Get,
44 "HEAD" => Self::Head,
45 "POST" => Self::Post,
46 "PUT" => Self::Put,
47 "DELETE" => Self::Delete,
48 "CONNECT" => Self::Connect,
49 "OPTIONS" => Self::Options,
50 "TRACE" => Self::Trace,
51 "PATCH" => Self::Patch,
52 _ => Self::Other(method.to_string()),
53 }
54 }
55}
56
57impl crate::TryFrom<wasi::http::types::Method> for Method {
58 type Error = method::InvalidMethod;
59
60 fn try_from(method: wasi::http::types::Method) -> Result<Self, Self::Error> {
61 match method {
62 wasi::http::types::Method::Get => Ok(Self::GET),
63 wasi::http::types::Method::Head => Ok(Self::HEAD),
64 wasi::http::types::Method::Post => Ok(Self::POST),
65 wasi::http::types::Method::Put => Ok(Self::PUT),
66 wasi::http::types::Method::Delete => Ok(Self::DELETE),
67 wasi::http::types::Method::Connect => Ok(Self::CONNECT),
68 wasi::http::types::Method::Options => Ok(Self::OPTIONS),
69 wasi::http::types::Method::Trace => Ok(Self::TRACE),
70 wasi::http::types::Method::Patch => Ok(Self::PATCH),
71 wasi::http::types::Method::Other(method) => method.parse(),
72 }
73 }
74}
75
76impl crate::From<uri::Scheme> for wasi::http::types::Scheme {
77 fn from(scheme: uri::Scheme) -> Self {
78 match scheme.as_str() {
79 "http" => Self::Http,
80 "https" => Self::Https,
81 _ => Self::Other(scheme.to_string()),
82 }
83 }
84}
85
86impl crate::TryFrom<wasi::http::types::Scheme> for http::uri::Scheme {
87 type Error = uri::InvalidUri;
88
89 fn try_from(scheme: wasi::http::types::Scheme) -> Result<Self, Self::Error> {
90 match scheme {
91 wasi::http::types::Scheme::Http => Ok(Self::HTTP),
92 wasi::http::types::Scheme::Https => Ok(Self::HTTPS),
93 wasi::http::types::Scheme::Other(scheme) => scheme.parse(),
94 }
95 }
96}
97
98#[derive(Debug)]
99pub enum FieldsToHeaderMapError {
100 InvalidHeaderName(header::InvalidHeaderName),
101 InvalidHeaderValue(header::InvalidHeaderValue),
102}
103
104impl Display for FieldsToHeaderMapError {
105 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106 match self {
107 FieldsToHeaderMapError::InvalidHeaderName(e) => write!(f, "invalid header name: {e}"),
108 FieldsToHeaderMapError::InvalidHeaderValue(e) => write!(f, "invalid header value: {e}"),
109 }
110 }
111}
112
113impl std::error::Error for FieldsToHeaderMapError {}
114
115impl crate::TryFrom<wasi::http::types::Fields> for HeaderMap {
116 type Error = FieldsToHeaderMapError;
117
118 fn try_from(fields: wasi::http::types::Fields) -> Result<Self, Self::Error> {
119 let mut headers = HeaderMap::new();
120 for (name, value) in fields.entries() {
121 let name =
122 HeaderName::try_from(name).map_err(FieldsToHeaderMapError::InvalidHeaderName)?;
123 let value =
124 HeaderValue::try_from(value).map_err(FieldsToHeaderMapError::InvalidHeaderValue)?;
125 match headers.entry(name) {
126 header::Entry::Vacant(entry) => {
127 entry.insert(value);
128 }
129 header::Entry::Occupied(mut entry) => {
130 entry.append(value);
131 }
132 };
133 }
134 Ok(headers)
135 }
136}
137
138impl crate::TryFrom<HeaderMap> for wasi::http::types::Fields {
139 type Error = wasi::http::types::HeaderError;
140
141 fn try_from(headers: HeaderMap) -> Result<Self, Self::Error> {
142 let fields = wasi::http::types::Fields::new();
143 for (name, value) in &headers {
144 fields.append(&name.to_string(), &value.as_bytes().to_vec())?;
145 }
146 Ok(fields)
147 }
148}
149
150pub trait OutgoingBody {
177 fn write(
178 self,
179 body: wasi::http::types::OutgoingBody,
180 stream: OutputStream,
181 ) -> std::io::Result<()>;
182}
183
184impl OutgoingBody for () {
185 fn write(
186 self,
187 _body: wasi::http::types::OutgoingBody,
188 _stream: OutputStream,
189 ) -> std::io::Result<()> {
190 Ok(())
191 }
192}
193
194impl OutgoingBody for &[u8] {
195 fn write(
196 self,
197 body: wasi::http::types::OutgoingBody,
198 mut stream: OutputStream,
199 ) -> std::io::Result<()> {
200 stream.write_all(self)?;
201 drop(stream);
202 wasi::http::types::OutgoingBody::finish(body, None)
203 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
204 }
205}
206
207impl OutgoingBody for Box<[u8]> {
208 fn write(
209 self,
210 body: wasi::http::types::OutgoingBody,
211 stream: OutputStream,
212 ) -> std::io::Result<()> {
213 self.as_ref().write(body, stream)
214 }
215}
216
217impl OutgoingBody for Vec<u8> {
218 fn write(
219 self,
220 body: wasi::http::types::OutgoingBody,
221 stream: OutputStream,
222 ) -> std::io::Result<()> {
223 self.as_slice().write(body, stream)
224 }
225}
226
227impl OutgoingBody for &str {
228 fn write(
229 self,
230 body: wasi::http::types::OutgoingBody,
231 stream: OutputStream,
232 ) -> std::io::Result<()> {
233 self.as_bytes().write(body, stream)
234 }
235}
236
237impl OutgoingBody for Box<str> {
238 fn write(
239 self,
240 body: wasi::http::types::OutgoingBody,
241 stream: OutputStream,
242 ) -> std::io::Result<()> {
243 self.as_ref().write(body, stream)
244 }
245}
246
247impl OutgoingBody for String {
248 fn write(
249 self,
250 body: wasi::http::types::OutgoingBody,
251 stream: OutputStream,
252 ) -> std::io::Result<()> {
253 self.as_str().write(body, stream)
254 }
255}
256
257impl OutgoingBody for InputStream {
258 fn write(
259 self,
260 body: wasi::http::types::OutgoingBody,
261 stream: OutputStream,
262 ) -> std::io::Result<()> {
263 loop {
264 match stream.blocking_splice(&self, u64::MAX) {
265 Ok(0) | Err(StreamError::Closed) => break,
266 Ok(_) => continue,
267 Err(StreamError::LastOperationFailed(err)) => {
268 return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
269 }
270 }
271 }
272 drop(stream);
273 wasi::http::types::OutgoingBody::finish(body, None)
274 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
275 }
276}
277
278impl OutgoingBody for wasi::http::types::IncomingBody {
279 fn write(
280 self,
281 body: wasi::http::types::OutgoingBody,
282 stream: OutputStream,
283 ) -> std::io::Result<()> {
284 let input = self
285 .stream()
286 .map_err(|()| std::io::Error::from(std::io::ErrorKind::Other))?;
287 loop {
288 match stream.blocking_splice(&input, u64::MAX) {
289 Ok(0) | Err(StreamError::Closed) => break,
290 Ok(_) => continue,
291 Err(StreamError::LastOperationFailed(err)) => {
292 return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
293 }
294 }
295 }
296 drop(stream);
297 let _trailers = wasi::http::types::IncomingBody::finish(self);
298 wasi::http::types::OutgoingBody::finish(body, None)
311 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
312 }
313}
314
315impl OutgoingBody for IncomingBody {
316 fn write(
317 self,
318 body: wasi::http::types::OutgoingBody,
319 stream: OutputStream,
320 ) -> std::io::Result<()> {
321 loop {
322 match stream.blocking_splice(&self.stream, u64::MAX) {
323 Ok(0) | Err(StreamError::Closed) => break,
324 Ok(_) => continue,
325 Err(StreamError::LastOperationFailed(err)) => {
326 return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
327 }
328 }
329 }
330 drop(stream);
331 let trailers = self
332 .into_trailers_wasi()
333 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
334 wasi::http::types::OutgoingBody::finish(body, trailers)
335 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
336 }
337}
338
339#[derive(Clone, Copy, Debug)]
341pub struct ReadBody<T>(T);
342
343impl<T: Read> OutgoingBody for ReadBody<T> {
344 fn write(
345 mut self,
346 body: wasi::http::types::OutgoingBody,
347 mut stream: OutputStream,
348 ) -> std::io::Result<()> {
349 std::io::copy(&mut self.0, &mut stream)?;
350 drop(stream);
351 wasi::http::types::OutgoingBody::finish(body, None)
352 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
353 }
354}
355
356pub struct IncomingBody {
362 stream: InputStream,
363 body: wasi::http::types::IncomingBody,
364}
365
366impl TryFrom<wasi::http::types::IncomingBody> for IncomingBody {
367 type Error = anyhow::Error;
368
369 fn try_from(body: wasi::http::types::IncomingBody) -> Result<Self, Self::Error> {
370 let stream = body
371 .stream()
372 .map_err(|()| anyhow!("failed to get incoming request body"))?;
373 Ok(Self { body, stream })
374 }
375}
376
377impl TryFrom<wasi::http::types::IncomingRequest> for IncomingBody {
378 type Error = anyhow::Error;
379
380 fn try_from(request: wasi::http::types::IncomingRequest) -> Result<Self, Self::Error> {
381 let body = request
382 .consume()
383 .map_err(|()| anyhow!("failed to consume incoming request"))?;
384 body.try_into()
385 }
386}
387
388impl Deref for IncomingBody {
389 type Target = InputStream;
390
391 fn deref(&self) -> &Self::Target {
392 &self.stream
393 }
394}
395
396impl DerefMut for IncomingBody {
397 fn deref_mut(&mut self) -> &mut Self::Target {
398 &mut self.stream
399 }
400}
401
402impl Read for IncomingBody {
403 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
404 Read::read(&mut self.stream, buf)
405 }
406}
407
408impl IncomingBody {
409 pub fn into_trailers(self) -> anyhow::Result<Option<HeaderMap>> {
410 let trailers = self.into_trailers_wasi()?;
411 let trailers = trailers.map(crate::TryInto::try_into).transpose()?;
412 Ok(trailers)
413 }
414
415 pub fn into_trailers_wasi(self) -> anyhow::Result<Option<wasi::http::types::Fields>> {
416 let IncomingBody { body, stream } = self;
417 drop(stream);
418 let _trailers = wasi::http::types::IncomingBody::finish(body);
419 Ok(None)
429 }
430}
431
432impl crate::TryFrom<wasi::http::types::IncomingRequest> for Request<IncomingBody> {
433 type Error = anyhow::Error;
434
435 fn try_from(request: wasi::http::types::IncomingRequest) -> Result<Self, Self::Error> {
436 let uri = Uri::builder();
437 let uri = if let Some(path_with_query) = request.path_with_query() {
438 uri.path_and_query(path_with_query)
439 } else {
440 uri.path_and_query("/")
441 };
442 let uri = if let Some(scheme) = request.scheme() {
443 let scheme = <uri::Scheme as crate::TryFrom<_>>::try_from(scheme)
444 .context("failed to convert scheme")?;
445 uri.scheme(scheme)
446 } else {
447 uri
448 };
449 let uri = if let Some(authority) = request.authority() {
450 uri.authority(authority)
451 } else {
452 uri
453 };
454 let uri = uri.build().context("failed to build URI")?;
455 let method = <Method as crate::TryFrom<_>>::try_from(request.method())
456 .context("failed to convert method")?;
457 let mut req = Request::builder().method(method).uri(uri);
458 let req_headers = req
459 .headers_mut()
460 .context("failed to construct header map")?;
461 *req_headers = crate::TryInto::try_into(request.headers())
462 .context("failed to convert header fields to header map")?;
463 let body = IncomingBody::try_from(request)?;
464 req.body(body).context("failed to construct request")
465 }
466}
467
468#[doc(hidden)]
469#[derive(Default, Debug, Copy, Clone)]
470pub struct IncomingHandler<T: ?Sized>(PhantomData<T>);
471
472pub enum ResponseError {
473 StatusCode(StatusCode),
475 Headers(wasi::http::types::HeaderError),
477 Body,
479 BodyStream,
481}
482
483pub trait Server {
486 fn handle(request: IncomingRequest) -> Result<Response<impl OutgoingBody>, ErrorCode>;
487
488 fn request_error(err: anyhow::Error) {
489 eprintln!("failed to convert `wasi:http/types.incoming-request` to `http::Request`: {err}");
490 }
491
492 fn response_error(out: ResponseOutparam, err: ResponseError) {
493 match err {
494 ResponseError::StatusCode(code) => {
495 ResponseOutparam::set(
496 out,
497 Err(ErrorCode::InternalError(Some(format!(
498 "code `{code}` is not a valid HTTP status code",
499 )))),
500 );
501 }
502 ResponseError::Headers(err) => {
503 ResponseOutparam::set(
504 out,
505 Err(ErrorCode::InternalError(Some(format!(
506 "{:#}",
507 anyhow!(err).context("failed to set headers"),
508 )))),
509 );
510 }
511 ResponseError::Body => {
512 ResponseOutparam::set(
513 out,
514 Err(ErrorCode::InternalError(Some(
515 "failed to get response body".into(),
516 ))),
517 );
518 }
519 ResponseError::BodyStream => {
520 ResponseOutparam::set(
521 out,
522 Err(ErrorCode::InternalError(Some(
523 "failed to get response body stream".into(),
524 ))),
525 );
526 }
527 }
528 }
529
530 fn body_error(err: std::io::Error) {
531 eprintln!("failed to write response body: {err}");
532 }
533}
534
535impl<T: Server + ?Sized> wasi::exports::http::incoming_handler::Guest for IncomingHandler<T> {
536 fn handle(
537 request: wasi::http::types::IncomingRequest,
538 response_out: wasi::http::types::ResponseOutparam,
539 ) {
540 match crate::TryInto::try_into(request) {
541 Ok(request) => match T::handle(request) {
542 Ok(response) => {
543 let (
544 response::Parts {
545 status, headers, ..
546 },
547 body,
548 ) = response.into_parts();
549
550 let headers = match crate::TryInto::try_into(headers) {
551 Ok(headers) => headers,
552 Err(err) => {
553 T::response_error(response_out, ResponseError::Headers(err));
554 return;
555 }
556 };
557 let resp_tx = OutgoingResponse::new(headers);
558 if let Err(()) = resp_tx.set_status_code(status.as_u16()) {
559 T::response_error(response_out, ResponseError::StatusCode(status));
560 return;
561 }
562
563 let Ok(resp_body) = resp_tx.body() else {
564 T::response_error(response_out, ResponseError::Body);
565 return;
566 };
567
568 let Ok(stream) = resp_body.write() else {
569 T::response_error(response_out, ResponseError::BodyStream);
570 return;
571 };
572
573 ResponseOutparam::set(response_out, Ok(resp_tx));
574 if let Err(err) = body.write(resp_body, stream) {
575 T::body_error(err);
576 }
577 }
578 Err(err) => ResponseOutparam::set(response_out, Err(err)),
579 },
580 Err(err) => T::request_error(err),
581 }
582 }
583}
584
585#[macro_export]
590macro_rules! export {
591 ($t:ty) => {
592 type __IncomingHandlerExport = ::wasmcloud_component::http::IncomingHandler<$t>;
593 ::wasmcloud_component::wasi::http::proxy::export!(__IncomingHandlerExport with_types_in ::wasmcloud_component::wasi);
594 };
595}
596pub use export;