diff --git a/crates/test-programs/src/bin/api_proxy.rs b/crates/test-programs/src/bin/api_proxy.rs index 6798d4e30fe2..ae75c1d3bf49 100644 --- a/crates/test-programs/src/bin/api_proxy.rs +++ b/crates/test-programs/src/bin/api_proxy.rs @@ -17,8 +17,8 @@ struct T; impl bindings::exports::wasi::http::incoming_handler::Guest for T { fn handle(_request: IncomingRequest, outparam: ResponseOutparam) { let hdrs = bindings::wasi::http::types::Headers::new(&[]); - let resp = bindings::wasi::http::types::OutgoingResponse::new(200, &hdrs); - let body = resp.write().expect("outgoing response"); + let resp = bindings::wasi::http::types::OutgoingResponse::new(200, hdrs); + let body = resp.body().expect("outgoing response"); bindings::wasi::http::types::ResponseOutparam::set(outparam, Ok(resp)); diff --git a/crates/test-programs/src/bin/api_proxy_streaming.rs b/crates/test-programs/src/bin/api_proxy_streaming.rs index f4978ac177cb..e63223058c55 100644 --- a/crates/test-programs/src/bin/api_proxy_streaming.rs +++ b/crates/test-programs/src/bin/api_proxy_streaming.rs @@ -51,11 +51,11 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam let response = OutgoingResponse::new( 200, - &Fields::new(&[("content-type".to_string(), b"text/plain".to_vec())]), + Fields::new(&[("content-type".to_string(), b"text/plain".to_vec())]), ); let mut body = - executor::outgoing_body(response.write().expect("response should be writable")); + executor::outgoing_body(response.body().expect("response should be writable")); ResponseOutparam::set(response_out, Ok(response)); @@ -75,7 +75,7 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam (Method::Post, Some("/echo")) => { let response = OutgoingResponse::new( 200, - &Fields::new( + Fields::new( &headers .into_iter() .filter_map(|(k, v)| (k == "content-type").then_some((k, v))) @@ -84,7 +84,7 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam ); let mut body = - executor::outgoing_body(response.write().expect("response should be writable")); + executor::outgoing_body(response.body().expect("response should be writable")); ResponseOutparam::set(response_out, Ok(response)); @@ -108,9 +108,9 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam } _ => { - let response = OutgoingResponse::new(405, &Fields::new(&[])); + let response = OutgoingResponse::new(405, Fields::new(&[])); - let body = response.write().expect("response should be writable"); + let body = response.body().expect("response should be writable"); ResponseOutparam::set(response_out, Ok(response)); @@ -137,7 +137,7 @@ async fn hash(url: &Url) -> Result { String::new() } )), - &Fields::new(&[]), + Fields::new(&[]), ); let response = executor::outgoing_request_send(request).await?; diff --git a/crates/test-programs/src/bin/http_outbound_request_response_build.rs b/crates/test-programs/src/bin/http_outbound_request_response_build.rs index b377b4a47955..d9a29822d33c 100644 --- a/crates/test-programs/src/bin/http_outbound_request_response_build.rs +++ b/crates/test-programs/src/bin/http_outbound_request_response_build.rs @@ -12,9 +12,9 @@ fn main() { None, Some(&http_types::Scheme::Https), Some("www.example.com"), - &headers, + headers, ); - let outgoing_body = request.write().unwrap(); + let outgoing_body = request.body().unwrap(); let request_body = outgoing_body.write().unwrap(); request_body .blocking_write_and_flush("request-body".as_bytes()) @@ -25,8 +25,8 @@ fn main() { "Content-Type".to_string(), "application/text".to_string().into_bytes(), )]); - let response = http_types::OutgoingResponse::new(200, &headers); - let outgoing_body = response.write().unwrap(); + let response = http_types::OutgoingResponse::new(200, headers); + let outgoing_body = response.body().unwrap(); let response_body = outgoing_body.write().unwrap(); response_body .blocking_write_and_flush("response-body".as_bytes()) diff --git a/crates/test-programs/src/http.rs b/crates/test-programs/src/http.rs index e87347cb5405..f065b45ee701 100644 --- a/crates/test-programs/src/http.rs +++ b/crates/test-programs/src/http.rs @@ -58,11 +58,11 @@ pub fn request( Some(path_with_query), Some(&scheme), Some(authority), - &headers, + headers, ); let outgoing_body = request - .write() + .body() .map_err(|_| anyhow!("outgoing request write failed"))?; if let Some(mut buf) = body { diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/http_impl.rs index d9e25a4ce490..f77575c9cec6 100644 --- a/crates/wasi-http/src/http_impl.rs +++ b/crates/wasi-http/src/http_impl.rs @@ -65,15 +65,22 @@ impl outgoing_handler::Host for T { } }; - let authority = if req.authority.find(':').is_some() { - req.authority.clone() + let authority = if let Some(authority) = req.authority { + if authority.find(':').is_some() { + authority + } else { + format!("{}:{port}", authority) + } } else { - format!("{}:{port}", req.authority) + String::new() }; let mut builder = hyper::Request::builder() .method(method) - .uri(format!("{scheme}{authority}{}", req.path_with_query)) + .uri(format!( + "{scheme}{authority}{}", + req.path_with_query.as_ref().map_or("", String::as_ref) + )) .header(hyper::header::HOST, &authority); for (k, v) in req.headers.iter() { diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs index 76530811ccec..50119b98977b 100644 --- a/crates/wasi-http/src/types.rs +++ b/crates/wasi-http/src/types.rs @@ -193,6 +193,51 @@ fn invalid_url(e: std::io::Error) -> anyhow::Error { )) } +impl From for types::Method { + fn from(method: http::Method) -> Self { + if method == http::Method::GET { + types::Method::Get + } else if method == hyper::Method::HEAD { + types::Method::Head + } else if method == hyper::Method::POST { + types::Method::Post + } else if method == hyper::Method::PUT { + types::Method::Put + } else if method == hyper::Method::DELETE { + types::Method::Delete + } else if method == hyper::Method::CONNECT { + types::Method::Connect + } else if method == hyper::Method::OPTIONS { + types::Method::Options + } else if method == hyper::Method::TRACE { + types::Method::Trace + } else if method == hyper::Method::PATCH { + types::Method::Patch + } else { + types::Method::Other(method.to_string()) + } + } +} + +impl TryInto for types::Method { + type Error = http::method::InvalidMethod; + + fn try_into(self) -> Result { + match self { + Method::Get => Ok(http::Method::GET), + Method::Head => Ok(http::Method::HEAD), + Method::Post => Ok(http::Method::POST), + Method::Put => Ok(http::Method::PUT), + Method::Delete => Ok(http::Method::DELETE), + Method::Connect => Ok(http::Method::CONNECT), + Method::Options => Ok(http::Method::OPTIONS), + Method::Trace => Ok(http::Method::TRACE), + Method::Patch => Ok(http::Method::PATCH), + Method::Other(s) => http::Method::from_bytes(s.as_bytes()), + } + } +} + pub struct HostIncomingRequest { pub parts: http::request::Parts, pub body: Option, @@ -206,8 +251,8 @@ pub struct HostResponseOutparam { pub struct HostOutgoingRequest { pub method: Method, pub scheme: Option, - pub path_with_query: String, - pub authority: String, + pub path_with_query: Option, + pub authority: Option, pub headers: FieldMap, pub body: Option, } diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs index dbaa54969fdd..d1c8eabb6df3 100644 --- a/crates/wasi-http/src/types_impl.rs +++ b/crates/wasi-http/src/types_impl.rs @@ -1,4 +1,4 @@ -use crate::bindings::http::types::{Error, Headers, Method, Scheme, StatusCode, Trailers}; +use crate::bindings::http::types::{self, Error, Headers, Method, Scheme, StatusCode, Trailers}; use crate::body::{FinishMessage, HostFutureTrailers, HostFutureTrailersState}; use crate::types::{HostIncomingRequest, HostOutgoingResponse}; use crate::WasiHttpView; @@ -19,6 +19,19 @@ use wasmtime_wasi::preview2::{ impl crate::bindings::http::types::Host for T {} +/// Take ownership of the underlying [`FieldMap`] associated with this fields resource. If the +/// fields resource references another fields, the returned [`FieldMap`] will be cloned. +fn move_fields(table: &mut Table, id: Resource) -> wasmtime::Result { + match table.delete(id)? { + HostFields::Ref { parent, get_fields } => { + let entry = table.get_any_mut(parent)?; + Ok(get_fields(entry).clone()) + } + + HostFields::Owned { fields } => Ok(fields), + } +} + fn get_fields_mut<'a>( table: &'a mut Table, id: &Resource, @@ -145,29 +158,8 @@ impl crate::bindings::http::types::HostFields for T { impl crate::bindings::http::types::HostIncomingRequest for T { fn method(&mut self, id: Resource) -> wasmtime::Result { - let method = self.table().get(&id)?.parts.method.as_ref(); - - if method == hyper::Method::GET { - Ok(Method::Get) - } else if method == hyper::Method::HEAD { - Ok(Method::Head) - } else if method == hyper::Method::POST { - Ok(Method::Post) - } else if method == hyper::Method::PUT { - Ok(Method::Put) - } else if method == hyper::Method::DELETE { - Ok(Method::Delete) - } else if method == hyper::Method::CONNECT { - Ok(Method::Connect) - } else if method == hyper::Method::OPTIONS { - Ok(Method::Options) - } else if method == hyper::Method::TRACE { - Ok(Method::Trace) - } else if method == hyper::Method::PATCH { - Ok(Method::Patch) - } else { - Ok(Method::Other(method.to_owned())) - } + let method = self.table().get(&id)?.parts.method.clone(); + Ok(method.into()) } fn path_with_query( &mut self, @@ -258,11 +250,12 @@ impl crate::bindings::http::types::HostOutgoingRequest for T { authority: Option, headers: Resource, ) -> wasmtime::Result> { - let headers = get_fields_mut(self.table(), &headers)?.clone(); + let headers = move_fields(self.table(), headers)?; + self.table() .push(HostOutgoingRequest { - path_with_query: path_with_query.unwrap_or("".to_string()), - authority: authority.unwrap_or("".to_string()), + path_with_query, + authority, method, headers, scheme, @@ -271,7 +264,7 @@ impl crate::bindings::http::types::HostOutgoingRequest for T { .context("[new_outgoing_request] pushing request") } - fn write( + fn body( &mut self, request: Resource, ) -> wasmtime::Result, ()>> { @@ -299,6 +292,97 @@ impl crate::bindings::http::types::HostOutgoingRequest for T { let _ = self.table().delete(request)?; Ok(()) } + + fn method( + &mut self, + request: wasmtime::component::Resource, + ) -> wasmtime::Result { + Ok(self.table().get(&request)?.method.clone().try_into()?) + } + + fn set_method( + &mut self, + request: wasmtime::component::Resource, + method: Method, + ) -> wasmtime::Result<()> { + self.table().get_mut(&request)?.method = method.into(); + Ok(()) + } + + fn path_with_query( + &mut self, + request: wasmtime::component::Resource, + ) -> wasmtime::Result> { + Ok(self.table().get(&request)?.path_with_query.clone()) + } + + fn set_path_with_query( + &mut self, + request: wasmtime::component::Resource, + path_with_query: Option, + ) -> wasmtime::Result<()> { + self.table().get_mut(&request)?.path_with_query = path_with_query; + Ok(()) + } + + fn scheme( + &mut self, + request: wasmtime::component::Resource, + ) -> wasmtime::Result> { + Ok(self.table().get(&request)?.scheme.clone()) + } + + fn set_scheme( + &mut self, + request: wasmtime::component::Resource, + scheme: Option, + ) -> wasmtime::Result<()> { + self.table().get_mut(&request)?.scheme = scheme; + Ok(()) + } + + fn authority( + &mut self, + request: wasmtime::component::Resource, + ) -> wasmtime::Result> { + Ok(self.table().get(&request)?.authority.clone()) + } + + fn set_authority( + &mut self, + request: wasmtime::component::Resource, + authority: Option, + ) -> wasmtime::Result<()> { + self.table().get_mut(&request)?.authority = authority; + Ok(()) + } + + fn headers( + &mut self, + request: wasmtime::component::Resource, + ) -> wasmtime::Result> { + let _ = self + .table() + .get(&request) + .context("[outgoing_request_headers] getting request")?; + + fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { + &mut elem + .downcast_mut::() + .unwrap() + .headers + } + + let id = self.table().push_child( + HostFields::Ref { + parent: request.rep(), + get_fields, + }, + &request, + )?; + + Ok(id) + } } impl crate::bindings::http::types::HostResponseOutparam for T { @@ -470,7 +554,7 @@ impl crate::bindings::http::types::HostOutgoingResponse for T { status: StatusCode, headers: Resource, ) -> wasmtime::Result> { - let fields = get_fields_mut(self.table(), &headers)?.clone(); + let fields = move_fields(self.table(), headers)?; let id = self.table().push(HostOutgoingResponse { status, @@ -481,7 +565,7 @@ impl crate::bindings::http::types::HostOutgoingResponse for T { Ok(id) } - fn write( + fn body( &mut self, id: Resource, ) -> wasmtime::Result, ()>> { @@ -500,6 +584,43 @@ impl crate::bindings::http::types::HostOutgoingResponse for T { Ok(Ok(id)) } + fn status_code( + &mut self, + id: Resource, + ) -> wasmtime::Result { + Ok(self.table().get(&id)?.status) + } + + fn set_status_code( + &mut self, + id: Resource, + status: types::StatusCode, + ) -> wasmtime::Result<()> { + self.table().get_mut(&id)?.status = status; + Ok(()) + } + + fn headers( + &mut self, + id: Resource, + ) -> wasmtime::Result> { + // Trap if the outgoing-response doesn't exist. + let _ = self.table().get(&id)?; + + fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { + let resp = elem.downcast_mut::().unwrap(); + &mut resp.headers + } + + Ok(self.table().push_child( + HostFields::Ref { + parent: id.rep(), + get_fields, + }, + &id, + )?) + } + fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { let _ = self.table().delete(id)?; Ok(()) diff --git a/crates/wasi-http/wit/deps/http/types.wit b/crates/wasi-http/wit/deps/http/types.wit index 343d3222020f..9cace67ab294 100644 --- a/crates/wasi-http/wit/deps/http/types.wit +++ b/crates/wasi-http/wit/deps/http/types.wit @@ -127,17 +127,73 @@ interface types { resource outgoing-request { /// Construct a new `outgoing-request`. + /// + /// * `method` represents the HTTP Method for the Request. + /// * `path-with-query` is the combination of the HTTP Path and Query for + /// the Request. When `none`, this represents an empty Path and empty + /// Query. + /// * `scheme` is the HTTP Related Scheme for the Request. When `none`, + /// the implementation may choose an appropriate default scheme. + /// * `authority` is the HTTP Authority for the Request. A value of `none` + /// may be used with Related Schemes which do not require an Authority. + /// The HTTP and HTTPS schemes always require an authority. + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. constructor( method: method, path-with-query: option, scheme: option, authority: option, - headers: borrow + headers: headers ); - /// Will return the outgoing-body child at most once. If called more than - /// once, subsequent calls will return error. - write: func() -> result; + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. + set-method: func(method: method); + + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + set-path-with-query: func(path-with-query: option); + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + set-scheme: func(scheme: option); + + /// Get the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + set-authority: func(authority: option); + + /// Get the headers associated with the Request. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; } /// Parameters for making an HTTP Request. Each of these parameters is an @@ -176,6 +232,9 @@ interface types { /// This method consumes the `response-outparam` to ensure that it is /// called at most once. If it is never called, the implementation /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. set: static func( param: response-outparam, response: result, @@ -260,16 +319,29 @@ interface types { resource outgoing-response { /// Construct an `outgoing-response`. - constructor(status-code: status-code, headers: borrow); + /// + /// * `status-code` is the HTTP Status Code for the Response. + /// * `headers` is the HTTP Headers for the Response. + constructor(status-code: status-code, headers: headers); + + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. + set-status-code: func(status-code: status-code); + + /// Get the headers associated with the Request. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; /// Returns the resource corresponding to the outgoing Body for this Response. /// /// Returns success on the first call: the `outgoing-body` resource for - /// this `outgoing-response` can be retrieved at most once. Sunsequent + /// this `outgoing-response` can be retrieved at most once. Subsequent /// calls will return error. - /// - /// FIXME: rename this method to `body`. - write: func() -> result; + body: func() -> result; } /// Represents an outgoing HTTP Request or Response's Body. diff --git a/crates/wasi/wit/deps/http/types.wit b/crates/wasi/wit/deps/http/types.wit index 343d3222020f..9cace67ab294 100644 --- a/crates/wasi/wit/deps/http/types.wit +++ b/crates/wasi/wit/deps/http/types.wit @@ -127,17 +127,73 @@ interface types { resource outgoing-request { /// Construct a new `outgoing-request`. + /// + /// * `method` represents the HTTP Method for the Request. + /// * `path-with-query` is the combination of the HTTP Path and Query for + /// the Request. When `none`, this represents an empty Path and empty + /// Query. + /// * `scheme` is the HTTP Related Scheme for the Request. When `none`, + /// the implementation may choose an appropriate default scheme. + /// * `authority` is the HTTP Authority for the Request. A value of `none` + /// may be used with Related Schemes which do not require an Authority. + /// The HTTP and HTTPS schemes always require an authority. + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. constructor( method: method, path-with-query: option, scheme: option, authority: option, - headers: borrow + headers: headers ); - /// Will return the outgoing-body child at most once. If called more than - /// once, subsequent calls will return error. - write: func() -> result; + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. + set-method: func(method: method); + + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + set-path-with-query: func(path-with-query: option); + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + set-scheme: func(scheme: option); + + /// Get the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + set-authority: func(authority: option); + + /// Get the headers associated with the Request. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; } /// Parameters for making an HTTP Request. Each of these parameters is an @@ -176,6 +232,9 @@ interface types { /// This method consumes the `response-outparam` to ensure that it is /// called at most once. If it is never called, the implementation /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. set: static func( param: response-outparam, response: result, @@ -260,16 +319,29 @@ interface types { resource outgoing-response { /// Construct an `outgoing-response`. - constructor(status-code: status-code, headers: borrow); + /// + /// * `status-code` is the HTTP Status Code for the Response. + /// * `headers` is the HTTP Headers for the Response. + constructor(status-code: status-code, headers: headers); + + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. + set-status-code: func(status-code: status-code); + + /// Get the headers associated with the Request. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; /// Returns the resource corresponding to the outgoing Body for this Response. /// /// Returns success on the first call: the `outgoing-body` resource for - /// this `outgoing-response` can be retrieved at most once. Sunsequent + /// this `outgoing-response` can be retrieved at most once. Subsequent /// calls will return error. - /// - /// FIXME: rename this method to `body`. - write: func() -> result; + body: func() -> result; } /// Represents an outgoing HTTP Request or Response's Body.