Repository: bytecodealliance/wasmtime
Pull request #7451 (2023-11-02 02:44:46)

wasi-http: Make all setters fallible
Rename the `header-error` variant to `validation-error`, and use that variant as the error value for all setters of `outbound-request` and `outbound-response`. This ensures that we have the opportunity to validate outbound request and response values before they're given to their respective handlers.

Validation for uri components is done by using `http:uri::Builder` with only that value, ensuring that we don't raise an error. Validation for method and scheme is only done for the `Other` cases, and is accomplished by using the fallible parsers for each type from the `http` crate.
<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->



Patch:
diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs
index 266fd4c36..a0531a741 100644
--- a/crates/wasi-http/src/types_impl.rs
+++ b/crates/wasi-http/src/types_impl.rs
@@ -1,19 +1,16 @@
-use crate::bindings::http::types::{
-    self, Error, HeaderError, Headers, Method, Scheme, StatusCode, Trailers,
-};
-use crate::body::{FinishMessage, HostFutureTrailers};
-use crate::types::{HostIncomingRequest, HostOutgoingResponse};
-use crate::WasiHttpView;
 use crate::{
-    body::{HostIncomingBody, HostOutgoingBody},
+    bindings::http::types::{self, Error, Headers, Method, Scheme, StatusCode, Trailers},
+    body::{FinishMessage, HostFutureTrailers, HostIncomingBody, HostOutgoingBody},
     types::{
-        FieldMap, HostFields, HostFutureIncomingResponse, HostIncomingResponse,
-        HostOutgoingRequest, HostResponseOutparam,
+        FieldMap, HostFields, HostFutureIncomingResponse, HostIncomingRequest,
+        HostIncomingResponse, HostOutgoingRequest, HostOutgoingResponse, HostResponseOutparam,
     },
+    WasiHttpView,
 };
 use anyhow::Context;
 use hyper::header::HeaderName;
 use std::any::Any;
+use std::str::FromStr;
 use wasmtime::component::Resource;
 use wasmtime_wasi::preview2::{
     bindings::io::streams::{InputStream, OutputStream},
@@ -85,22 +82,22 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
     fn from_list(
         &mut self,
         entries: Vec<(String, Vec<u8>)>,
-    ) -> wasmtime::Result<Result<Resource<HostFields>, HeaderError>> {
+    ) -> wasmtime::Result<Result<Resource<HostFields>, types::HeaderError>> {
         let mut map = hyper::HeaderMap::new();
 
         for (header, value) in entries {
             let header = match hyper::header::HeaderName::from_bytes(header.as_bytes()) {
                 Ok(header) => header,
-                Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+                Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)),
             };
 
             if is_forbidden_header(self, &header) {
-                return Ok(Err(HeaderError::Forbidden));
+                return Ok(Err(types::HeaderError::Forbidden));
             }
 
             let value = match hyper::header::HeaderValue::from_bytes(&value) {
                 Ok(value) => value,
-                Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+                Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)),
             };
 
             map.append(header, value);
@@ -145,21 +142,21 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
         fields: Resource<HostFields>,
         name: String,
         byte_values: Vec<Vec<u8>>,
-    ) -> wasmtime::Result<Result<(), HeaderError>> {
+    ) -> wasmtime::Result<Result<(), types::HeaderError>> {
         let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
             Ok(header) => header,
-            Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+            Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)),
         };
 
         if is_forbidden_header(self, &header) {
-            return Ok(Err(HeaderError::Forbidden));
+            return Ok(Err(types::HeaderError::Forbidden));
         }
 
         let mut values = Vec::with_capacity(byte_values.len());
         for value in byte_values {
             match hyper::header::HeaderValue::from_bytes(&value) {
                 Ok(value) => values.push(value),
-                Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+                Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)),
             }
         }
 
@@ -189,19 +186,19 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
         fields: Resource<HostFields>,
         name: String,
         value: Vec<u8>,
-    ) -> wasmtime::Result<Result<(), HeaderError>> {
+    ) -> wasmtime::Result<Result<(), types::HeaderError>> {
         let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
             Ok(header) => header,
-            Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+            Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)),
         };
 
         if is_forbidden_header(self, &header) {
-            return Ok(Err(HeaderError::Forbidden));
+            return Ok(Err(types::HeaderError::Forbidden));
         }
 
         let value = match hyper::header::HeaderValue::from_bytes(&value) {
             Ok(value) => value,
-            Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+            Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)),
         };
 
         let m = get_fields_mut(self.table(), &fields)
@@ -325,21 +322,17 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostIncomingRequest for T {
 impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
     fn new(
         &mut self,
-        method: Method,
-        path_with_query: Option<String>,
-        scheme: Option<Scheme>,
-        authority: Option<String>,
         headers: Resource<Headers>,
     ) -> wasmtime::Result<Resource<HostOutgoingRequest>> {
         let headers = move_fields(self.table(), headers)?;
 
         self.table()
             .push(HostOutgoingRequest {
-                path_with_query,
-                authority,
-                method,
+                path_with_query: None,
+                authority: None,
+                method: types::Method::Get,
                 headers,
-                scheme,
+                scheme: None,
                 body: None,
             })
             .context("[new_outgoing_request] pushing request")
@@ -385,9 +378,18 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
         &mut self,
         request: wasmtime::component::Resource<types::OutgoingRequest>,
         method: Method,
-    ) -> wasmtime::Result<()> {
-        self.table().get_mut(&request)?.method = method.into();
-        Ok(())
+    ) -> wasmtime::Result<Result<(), ()>> {
+        let req = self.table().get_mut(&request)?;
+
+        if let Method::Other(s) = &method {
+            if let Err(_) = http::Method::from_str(s) {
+                return Ok(Err(()));
+            }
+        }
+
+        req.method = method;
+
+        Ok(Ok(()))
     }
 
     fn path_with_query(
@@ -401,9 +403,18 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
         &mut self,
         request: wasmtime::component::Resource<types::OutgoingRequest>,
         path_with_query: Option<String>,
-    ) -> wasmtime::Result<()> {
-        self.table().get_mut(&request)?.path_with_query = path_with_query;
-        Ok(())
+    ) -> wasmtime::Result<Result<(), ()>> {
+        let req = self.table().get_mut(&request)?;
+
+        if let Some(s) = path_with_query.as_ref() {
+            if let Err(_) = http::uri::PathAndQuery::from_str(s) {
+                return Ok(Err(()));
+            }
+        }
+
+        req.path_with_query = path_with_query;
+
+        Ok(Ok(()))
     }
 
     fn scheme(
@@ -417,9 +428,18 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
         &mut self,
         request: wasmtime::component::Resource<types::OutgoingRequest>,
         scheme: Option<Scheme>,
-    ) -> wasmtime::Result<()> {
-        self.table().get_mut(&request)?.scheme = scheme;
-        Ok(())
+    ) -> wasmtime::Result<Result<(), ()>> {
+        let req = self.table().get_mut(&request)?;
+
+        if let Some(types::Scheme::Other(s)) = scheme.as_ref() {
+            if let Err(_) = http::uri::Scheme::from_str(s.as_str()) {
+                return Ok(Err(()));
+            }
+        }
+
+        req.scheme = scheme;
+
+        Ok(Ok(()))
     }
 
     fn authority(
@@ -433,9 +453,24 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
         &mut self,
         request: wasmtime::component::Resource<types::OutgoingRequest>,
         authority: Option<String>,
-    ) -> wasmtime::Result<()> {
-        self.table().get_mut(&request)?.authority = authority;
-        Ok(())
+    ) -> wasmtime::Result<Result<(), ()>> {
+        let req = self.table().get_mut(&request)?;
+
+        if let Some(s) = authority.as_ref() {
+            println!("checking authority {s}");
+            let auth = match http::uri::Authority::from_str(s.as_str()) {
+                Ok(auth) => auth,
+                Err(_) => return Ok(Err(())),
+            };
+
+            if s.contains(':') && auth.port_u16().is_none() {
+                return Ok(Err(()));
+            }
+        }
+
+        req.authority = authority;
+
+        Ok(Ok(()))
     }
 
     fn headers(
@@ -675,16 +710,12 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingResponse for T {
         &mut self,
         id: Resource<HostOutgoingResponse>,
         status: types::StatusCode,
-    ) -> wasmtime::Result<Result<(), Error>> {
+    ) -> wasmtime::Result<Result<(), ()>> {
         let resp = self.table().get_mut(&id)?;
 
         match http::StatusCode::from_u16(status) {
             Ok(status) => resp.status = status,
-            Err(_) => {
-                return Ok(Err(Error::UnexpectedError(
-                    "Invalid status code".to_string(),
-                )))
-            }
+            Err(_) => return Ok(Err(())),
         };
 
         Ok(Ok(()))
diff --git a/crates/wasi-http/wit/deps/http/types.wit b/crates/wasi-http/wit/deps/http/types.wit
index 81166b011..e3c06ae9f 100644
--- a/crates/wasi-http/wit/deps/http/types.wit
+++ b/crates/wasi-http/wit/deps/http/types.wit
@@ -148,17 +148,9 @@ interface types {
   /// Represents an outgoing HTTP Request.
   resource outgoing-request {
 
-    /// Construct a new `outgoing-request`.
+    /// Construct a new `outgoing-request` with a default `method` of `GET`, and
+    /// default values for `path-with-query`, `scheme`, and `authority.
     ///
-    /// * `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
@@ -167,10 +159,6 @@ interface types {
     /// It is the obligation of the `outgoing-handler.handle` implementation
     /// to reject invalid constructions of `outgoing-request`.
     constructor(
-      method: method,
-      path-with-query: option<string>,
-      scheme: option<scheme>,
-      authority: option<string>,
       headers: headers
     );
 
@@ -184,22 +172,25 @@ interface types {
 
     /// Get the Method for the Request.
     method: func() -> method;
-    /// Set the Method for the Request.
-    set-method: func(method: method);
+    /// Set the Method for the Request. Fails if the string present in a
+    /// `method.other` argument is not a syntactically valid method.
+    set-method: func(method: method) -> result;
 
     /// 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<string>;
     /// 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<string>);
+    /// When `none`, this represents an empty Path and empty Query. Fails is the
+    /// string given is not a syntactically valid path and query uri component.
+    set-path-with-query: func(path-with-query: option<string>) -> result;
 
     /// Get the HTTP Related Scheme for the Request. When `none`, the
     /// implementation may choose an appropriate default scheme.
     scheme: func() -> option<scheme>;
     /// Set the HTTP Related Scheme for the Request. When `none`, the
-    /// implementation may choose an appropriate default scheme.
-    set-scheme: func(scheme: option<scheme>);
+    /// implementation may choose an appropriate default scheme. Fails if the
+    /// string given is not a syntactically valid uri scheme.
+    set-scheme: func(scheme: option<scheme>) -> result;
 
     /// 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
@@ -207,8 +198,9 @@ interface types {
     authority: func() -> option<string>;
     /// 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<string>);
+    /// HTTPS schemes always require an authority. Fails if the string given is
+    /// not a syntactically valid uri authority.
+    set-authority: func(authority: option<string>) -> result;
 
     /// Get the headers associated with the Request.
     ///
@@ -361,8 +353,9 @@ interface types {
     /// 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) -> result<_, error>;
+    /// Set the HTTP Status Code for the Response. Fails if the status-code
+    /// given is not a valid http status code.
+    set-status-code: func(status-code: status-code) -> result;
 
     /// Get the headers associated with the Request.
     ///
diff --git a/crates/wasi/wit/deps/http/types.wit b/crates/wasi/wit/deps/http/types.wit
index 81166b011..e3c06ae9f 100644
--- a/crates/wasi/wit/deps/http/types.wit
+++ b/crates/wasi/wit/deps/http/types.wit
@@ -148,17 +148,9 @@ interface types {
   /// Represents an outgoing HTTP Request.
   resource outgoing-request {
 
-    /// Construct a new `outgoing-request`.
+    /// Construct a new `outgoing-request` with a default `method` of `GET`, and
+    /// default values for `path-with-query`, `scheme`, and `authority.
     ///
-    /// * `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
@@ -167,10 +159,6 @@ interface types {
     /// It is the obligation of the `outgoing-handler.handle` implementation
     /// to reject invalid constructions of `outgoing-request`.
     constructor(
-      method: method,
-      path-with-query: option<string>,
-      scheme: option<scheme>,
-      authority: option<string>,
       headers: headers
     );
 
@@ -184,22 +172,25 @@ interface types {
 
     /// Get the Method for the Request.
     method: func() -> method;
-    /// Set the Method for the Request.
-    set-method: func(method: method);
+    /// Set the Method for the Request. Fails if the string present in a
+    /// `method.other` argument is not a syntactically valid method.
+    set-method: func(method: method) -> result;
 
     /// 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<string>;
     /// 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<string>);
+    /// When `none`, this represents an empty Path and empty Query. Fails is the
+    /// string given is not a syntactically valid path and query uri component.
+    set-path-with-query: func(path-with-query: option<string>) -> result;
 
     /// Get the HTTP Related Scheme for the Request. When `none`, the
     /// implementation may choose an appropriate default scheme.
     scheme: func() -> option<scheme>;
     /// Set the HTTP Related Scheme for the Request. When `none`, the
-    /// implementation may choose an appropriate default scheme.
-    set-scheme: func(scheme: option<scheme>);
+    /// implementation may choose an appropriate default scheme. Fails if the
+    /// string given is not a syntactically valid uri scheme.
+    set-scheme: func(scheme: option<scheme>) -> result;
 
     /// 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
@@ -207,8 +198,9 @@ interface types {
     authority: func() -> option<string>;
     /// 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<string>);
+    /// HTTPS schemes always require an authority. Fails if the string given is
+    /// not a syntactically valid uri authority.
+    set-authority: func(authority: option<string>) -> result;
 
     /// Get the headers associated with the Request.
     ///
@@ -361,8 +353,9 @@ interface types {
     /// 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) -> result<_, error>;
+    /// Set the HTTP Status Code for the Response. Fails if the status-code
+    /// given is not a valid http status code.
+    set-status-code: func(status-code: status-code) -> result;
 
     /// Get the headers associated with the Request.
     ///


Test patch:
diff --git a/crates/test-programs/src/bin/api_proxy_streaming.rs b/crates/test-programs/src/bin/api_proxy_streaming.rs
index 730bef938..92ad6fb02 100644
--- a/crates/test-programs/src/bin/api_proxy_streaming.rs
+++ b/crates/test-programs/src/bin/api_proxy_streaming.rs
@@ -1,4 +1,4 @@
-use anyhow::{bail, Result};
+use anyhow::{anyhow, bail, Result};
 use bindings::wasi::http::types::{
     Fields, IncomingRequest, IncomingResponse, Method, OutgoingBody, OutgoingRequest,
     OutgoingResponse, ResponseOutparam, Scheme,
@@ -174,15 +174,26 @@ async fn double_echo(
     incoming_request: IncomingRequest,
     url: &Url,
 ) -> Result<(impl Future<Output = Result<()>>, IncomingResponse)> {
-    let outgoing_request = OutgoingRequest::new(
-        &Method::Post,
-        Some(url.path()),
-        Some(&match url.scheme() {
+    let outgoing_request = OutgoingRequest::new(Fields::new());
+
+    outgoing_request
+        .set_method(&Method::Post)
+        .map_err(|()| anyhow!("failed to set method"))?;
+
+    outgoing_request
+        .set_path_with_query(Some(url.path()))
+        .map_err(|()| anyhow!("failed to set path_with_query"))?;
+
+    outgoing_request
+        .set_scheme(Some(&match url.scheme() {
             "http" => Scheme::Http,
             "https" => Scheme::Https,
             scheme => Scheme::Other(scheme.into()),
-        }),
-        Some(&format!(
+        }))
+        .map_err(|()| anyhow!("failed to set scheme"))?;
+
+    outgoing_request
+        .set_authority(Some(&format!(
             "{}{}",
             url.host_str().unwrap_or(""),
             if let Some(port) = url.port() {
@@ -190,9 +201,8 @@ async fn double_echo(
             } else {
                 String::new()
             }
-        )),
-        Fields::new(),
-    );
+        )))
+        .map_err(|()| anyhow!("failed to set authority"))?;
 
     let mut body = executor::outgoing_body(
         outgoing_request
@@ -252,15 +262,20 @@ fn respond(status: u16, response_out: ResponseOutparam) {
 }
 
 async fn hash(url: &Url) -> Result<String> {
-    let request = OutgoingRequest::new(
-        &Method::Get,
-        Some(url.path()),
-        Some(&match url.scheme() {
+    let request = OutgoingRequest::new(Fields::new());
+
+    request
+        .set_path_with_query(Some(url.path()))
+        .map_err(|()| anyhow!("failed to set path_with_query"))?;
+    request
+        .set_scheme(Some(&match url.scheme() {
             "http" => Scheme::Http,
             "https" => Scheme::Https,
             scheme => Scheme::Other(scheme.into()),
-        }),
-        Some(&format!(
+        }))
+        .map_err(|()| anyhow!("failed to set scheme"))?;
+    request
+        .set_authority(Some(&format!(
             "{}{}",
             url.host_str().unwrap_or(""),
             if let Some(port) = url.port() {
@@ -268,9 +283,8 @@ async fn hash(url: &Url) -> Result<String> {
             } else {
                 String::new()
             }
-        )),
-        Fields::new(),
-    );
+        )))
+        .map_err(|()| anyhow!("failed to set authority"))?;
 
     let response = executor::outgoing_request_send(request).await?;
 
diff --git a/crates/test-programs/src/bin/http_outbound_request_invalid_port.rs b/crates/test-programs/src/bin/http_outbound_request_invalid_port.rs
index cb189eb93..e49690ce0 100644
--- a/crates/test-programs/src/bin/http_outbound_request_invalid_port.rs
+++ b/crates/test-programs/src/bin/http_outbound_request_invalid_port.rs
@@ -10,9 +10,5 @@ fn main() {
         None,
     );
 
-    let error = res.unwrap_err();
-    assert_eq!(
-        error.to_string(),
-        "Error::InvalidUrl(\"invalid port value\")"
-    );
+    assert!(res.is_err());
 }
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 c2360d299..59664ccde 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
@@ -8,13 +8,18 @@ fn main() {
             "application/json".to_string().into_bytes(),
         )])
         .unwrap();
-        let request = http_types::OutgoingRequest::new(
-            &http_types::Method::Get,
-            None,
-            Some(&http_types::Scheme::Https),
-            Some("www.example.com"),
-            headers,
-        );
+        let request = http_types::OutgoingRequest::new(headers);
+
+        request
+            .set_method(&http_types::Method::Get)
+            .expect("setting method");
+        request
+            .set_scheme(Some(&http_types::Scheme::Https))
+            .expect("setting scheme");
+        request
+            .set_authority(Some("www.example.com"))
+            .expect("setting authority");
+
         let outgoing_body = request.body().unwrap();
         let request_body = outgoing_body.write().unwrap();
         request_body
@@ -34,5 +39,24 @@ fn main() {
             .blocking_write_and_flush("response-body".as_bytes())
             .unwrap();
     }
+
+    {
+        let req = http_types::OutgoingRequest::new(http_types::Fields::new());
+
+        assert!(req
+            .set_method(&http_types::Method::Other("invalid method".to_string()))
+            .is_err());
+
+        assert!(req.set_authority(Some("bad-port:99999")).is_err());
+        assert!(req.set_authority(Some("bad-\nhost")).is_err());
+        assert!(req.set_authority(Some("too-many-ports:80:80:80")).is_err());
+
+        assert!(req
+            .set_scheme(Some(&http_types::Scheme::Other("bad\nscheme".to_string())))
+            .is_err());
+
+        assert!(req.set_path_with_query(Some("/bad\npath")).is_err());
+    }
+
     println!("Done");
 }
diff --git a/crates/test-programs/src/http.rs b/crates/test-programs/src/http.rs
index 1f16348f2..3dbcb6846 100644
--- a/crates/test-programs/src/http.rs
+++ b/crates/test-programs/src/http.rs
@@ -52,13 +52,20 @@ pub fn request(
         .concat(),
     )?;
 
-    let request = http_types::OutgoingRequest::new(
-        &method,
-        Some(path_with_query),
-        Some(&scheme),
-        Some(authority),
-        headers,
-    );
+    let request = http_types::OutgoingRequest::new(headers);
+
+    request
+        .set_method(&method)
+        .map_err(|()| anyhow!("failed to set method"))?;
+    request
+        .set_scheme(Some(&scheme))
+        .map_err(|()| anyhow!("failed to set scheme"))?;
+    request
+        .set_authority(Some(authority))
+        .map_err(|()| anyhow!("failed to set authority"))?;
+    request
+        .set_path_with_query(Some(&path_with_query))
+        .map_err(|()| anyhow!("failed to set path_with_query"))?;
 
     let outgoing_body = request
         .body()