From 408afa78b7f2be72ce3cae8174fb56ff1faf457d Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Fri, 6 Sep 2024 14:41:25 +0200 Subject: [PATCH] Add incoming and outgoing handlers for HTTP/2 This PR adds the interfaces `http2-incoming-handler` and `http2-outgoing-handler` and the world `http2-proxy`, which exports and imports those interfaces, respectively. Additionally, it adds the variant `types.http-version` and a method `http-version()-> http-version` to both `types.incoming-request` and `types.incoming-response`. The motivation for these additions is that there are use cases that require specifically HTTP/2 to be used for both incoming and outgoing connections, so wasi-http should provide a way to enforce this requirement. Most notably, gRPC is specified as layered on top of HTTP/2, not HTTP more generally, so attempting to transport it over HTTP/1.1 or HTTP/3 would fail. An alternative API design would be to add a method each to `types.{ outgoing-request, outgoing-response }` to opt in to HTTP/2. I think the design proposed here is better in two regards: 1. It makes static analysis of an application's intent much easier: instead of having to check for use of a `set-http-version` method on outgoing requests/responses and then making assumptions about which version that might opt in to, the normal validations for targeted worlds is sufficient. 2. It matches what other APIs are doing and should make it easier to implement them in terms of wasi-http. As an example of the latter point, Go's standard library uses the same approach: Go's [`net/http` package](https://pkg.go.dev/net/http@go1.23.0#hdr-HTTP_2) transparently supports multiple HTTP versions, and explicitly opting in to HTTP/2 is possible using the [`x/net/http2` package](https://pkg.go.dev/golang.org/x/net/http2). Similarly, the Rust ecosystem's Hyper crate supports [automatically choosing](https://docs.rs/hyper/latest/hyper/server/conn/index.html) an HTTP version, or using `http1` or `http2` modules specifically. In both cases, the other types in the HTTP APIs remain the same, as in this proposal. Signed-off-by: Till Schneidereit --- wit/http2-handler.wit | 57 +++++++++++++++++++++++++++++++++++++++++++ wit/proxy.wit | 45 +++++++++++++++++++++++++++++++--- wit/types.wit | 22 +++++++++++++++-- 3 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 wit/http2-handler.wit diff --git a/wit/http2-handler.wit b/wit/http2-handler.wit new file mode 100644 index 0000000..bfde8a7 --- /dev/null +++ b/wit/http2-handler.wit @@ -0,0 +1,57 @@ +/// This interface defines a handler of incoming HTTP/2 Requests. It should +/// be exported by components which can respond to HTTP/2 Requests. +/// +/// Note: This interface specifically enforces use of HTTP/2, and must not be +/// provided in environments that aren't able to provide an HTTP/2 connection +/// or otherwise implement the full semantics of an HTTP/2 connection. +@since(version = 0.2.2) +interface http2-incoming-handler { + @since(version = 0.2.2) + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + @since(version = 0.2.2) + handle: func( + request: incoming-request, + response-out: response-outparam + ); +} + +/// This interface defines a handler of outgoing HTTP/2 Requests. It should be +/// imported by components which wish to make HTTP/2 Requests. +/// +/// Note: This interface specifically enforces use of HTTP/2, and must not be +/// provided in environments that aren't able to provide an HTTP/2 connection +/// or otherwise implement the full semantics of an HTTP/2 connection. +@since(version = 0.2.2) +interface http2-outgoing-handler { + @since(version = 0.2.2) + use types.{ + outgoing-request, request-options, future-incoming-response, error-code + }; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + @since(version = 0.2.2) + handle: func( + request: outgoing-request, + options: option + ) -> result; +} diff --git a/wit/proxy.wit b/wit/proxy.wit index 415d2ee..5b09fc1 100644 --- a/wit/proxy.wit +++ b/wit/proxy.wit @@ -1,9 +1,10 @@ -package wasi:http@0.2.1; +package wasi:http@0.2.2; -/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. +/// The `wasi:http/imports-no-handler` world imports all the APIs for HTTP proxies +/// except for the `outgoing-handler`. /// It is intended to be `include`d in other worlds. -@since(version = 0.2.0) -world imports { +@since(version = 0.2.2) +world imports-no-handler { /// HTTP proxies have access to time and randomness. @since(version = 0.2.0) import wasi:clocks/monotonic-clock@0.2.1; @@ -25,6 +26,14 @@ world imports { /// when this import is properly removed. @since(version = 0.2.0) import wasi:cli/stdin@0.2.1; +} + +/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. +/// It is intended to be `include`d in other worlds. +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + include imports-no-handler; /// This is the default handler to use when user code simply wants to make an /// HTTP request (e.g., via `fetch()`). @@ -36,6 +45,8 @@ world imports { /// hosts that includes HTTP forward and reverse proxies. Components targeting /// this world may concurrently stream in and out any number of incoming and /// outgoing HTTP requests. +/// `wasi:http/proxy` doesn't specify the HTTP version to be used, leaving that +/// decision to the host. @since(version = 0.2.0) world proxy { @since(version = 0.2.0) @@ -48,3 +59,29 @@ world proxy { @since(version = 0.2.0) export incoming-handler; } + +/// The `wasi:http/http2-proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP/2 forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +/// In difference to `wasi:http/proxy`, `wasi:http/http2-proxy` specifies that exactly +/// HTTP/2 must be used, making it usable for scenarios that require HTTP/2. +/// In particular, gRPC must use HTTP/2 as the transport, so gRPC clients and servers +/// should target this world. +@since(version = 0.2.2) +world http2-proxy { + @since(version = 0.2.2) + include imports-no-handler; + + /// This is the default handler to use when user code simply wants to make an + /// HTTP request (e.g., via `fetch()`). + @since(version = 0.2.2) + import http2-outgoing-handler; + + /// The host delivers incoming HTTP requests to a component by calling the + /// `handle` function of this exported interface. A host may arbitrarily reuse + /// or not reuse component instance when delivering incoming HTTP requests and + /// thus a component must be able to handle 0..N calls to `handle`. + @since(version = 0.2.2) + export http2-incoming-handler; +} diff --git a/wit/types.wit b/wit/types.wit index 30b3642..e8c1552 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -34,6 +34,16 @@ interface types { HTTPS, other(string) } + } + + /// This type corresponds to HTTP protocol versions. + @since(version = 0.2.2) + variant http-version { + HTTP11, + HTTP2, + HTTP3 + other(string) + } /// These cases are inspired by the IANA HTTP Proxy Error Types: /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types @@ -149,9 +159,9 @@ interface types { /// /// Field keys should always be treated as case insensitive by the `fields` /// resource for the purposes of equality checking. - /// + /// /// # Deprecation - /// + /// /// This type has been deprecated in favor of the `field-name` type. @since(version = 0.2.0) @deprecated(version = 0.2.2) @@ -273,6 +283,10 @@ interface types { @since(version = 0.2.0) resource incoming-request { + /// Returns the HTTP version of the incoming request. + @since(version = 0.2.2) + http-version: func() -> http-version; + /// Returns the method of the incoming request. @since(version = 0.2.0) method: func() -> method; @@ -460,6 +474,10 @@ interface types { @since(version = 0.2.0) resource incoming-response { + /// Returns the HTTP version of the incoming request. + @since(version = 0.2.2) + http-version: func() -> http-version; + /// Returns the status code from the incoming response. @since(version = 0.2.0) status: func() -> status-code;