From b154defc3761b26332fda20e7284b9238ccffdb3 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 6 Oct 2023 15:40:41 -0700 Subject: [PATCH] Use resources, and restructure body handling --- wit/handler.wit | 18 ++-- wit/types.wit | 245 ++++++++++++++++++++++++++---------------------- 2 files changed, 144 insertions(+), 119 deletions(-) diff --git a/wit/handler.wit b/wit/handler.wit index 372921e..3140d8c 100644 --- a/wit/handler.wit +++ b/wit/handler.wit @@ -12,13 +12,11 @@ interface incoming-handler { // The `handle` function takes an outparam instead of returning its response // so that the component may stream its response while streaming any other // request or response bodies. The callee MUST write a response to the - // `response-out` and then finish the response before returning. The caller - // is expected to start streaming the response once `set-response-outparam` - // is called and finish streaming the response when `drop-response-outparam` - // is called. The `handle` function is then allowed to continue executing - // any post-response logic before returning. While this post-response - // execution is taken off the critical path, since there is no return value, - // there is no way to report its success or failure. + // `response-outparam` and then finish the response before returning. The `handle` + // function is allowed to continue execution after finishing the response's + // output stream. While this post-response execution is taken off the + // critical path, since there is no return value, there is no way to report + // its success or failure. handle: func( request: incoming-request, response-out: response-outparam @@ -33,13 +31,15 @@ interface incoming-handler { // that takes a `request` parameter and returns a `response` result. // interface outgoing-handler { - use types.{outgoing-request, request-options, future-incoming-response}; + use types.{outgoing-request, request-options, future-incoming-response, error}; // The parameter and result types of the `handle` function allow the caller // to concurrently stream the bodies of the outgoing request and the incoming // response. + // Consumes the outgoing-request. Gives an error if the outgoing-request + // is invalid or cannot be satisfied by this handler. handle: func( request: outgoing-request, options: option - ) -> future-incoming-response; + ) -> result; } diff --git a/wit/types.wit b/wit/types.wit index 3670233..c1e4ae1 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -4,7 +4,7 @@ interface types { use wasi:io/streams.{input-stream, output-stream}; use wasi:io/poll.{pollable}; - + // This type corresponds to HTTP standard Methods. variant method { get, @@ -30,101 +30,84 @@ interface types { // This type enumerates the different kinds of errors that may occur when // initially returning a response. variant error { - invalid-url(string), - timeout-error(string), - protocol-error(string), - unexpected-error(string) + invalid-url(string), + timeout-error(string), + protocol-error(string), + unexpected-error(string) } // This following block defines the `fields` resource which corresponds to // HTTP standard Fields. Soon, when resource types are added, the `type // fields = u32` type alias can be replaced by a proper `resource fields` // definition containing all the functions using the method syntactic sugar. - type fields = u32; - drop-fields: func(fields: fields); - new-fields: func(entries: list>>) -> fields; - // Returns an empty list if `name` is not present. - fields-get: func(fields: fields, name: string) -> list>; - fields-set: func(fields: fields, name: string, value: list>); - fields-delete: func(fields: fields, name: string); - fields-append: func(fields: fields, name: string, value: list); - fields-entries: func(fields: fields) -> list>>; - fields-clone: func(fields: fields) -> fields; + resource fields { + // Multiple values for a header are multiple entries in the list with the + // same key. + constructor(entries: list>>); + + // Values off wire are not necessarily well formed, so they are given by + // list instead of string. + get: func(name: string) -> list>; + + // Values off wire are not necessarily well formed, so they are given by + // list instead of string. + set: func(name: string, value: list>); + delete: func(name: string); + append: func(name: string, value: list); + + // Values off wire are not necessarily well formed, so they are given by + // list instead of string. + entries: func() -> list>>; + + // Deep copy of all contents in a fields. + clone: func() -> fields; + } type headers = fields; type trailers = fields; - // The following block defines stream types which corresponds to the HTTP - // standard Contents and Trailers. With Preview3, all of these fields can be - // replaced by a stream>. In the interim, we need to - // build on separate resource types defined by `wasi:io/streams`. The - // `finish-` functions emulate the stream's result value and MUST be called - // exactly once after the final read/write from/to the stream before dropping - // the stream. The optional `future-` types describe the asynchronous result of - // reading/writing the optional HTTP trailers and MUST be waited on and dropped - // to complete streaming the request/response. - type incoming-stream = input-stream; - type outgoing-stream = output-stream; - finish-incoming-stream: func(s: incoming-stream) -> option; - finish-outgoing-stream: func(s: outgoing-stream); - finish-outgoing-stream-with-trailers: func(s: outgoing-stream, trailers: trailers) -> future-write-trailers-result; - - // The following block defines the `future-trailers` resource, which is - // returned when finishing an `incoming-stream` to asychronously produce the - // final trailers. - type future-trailers = u32; - drop-future-trailers: func(f: future-trailers); - future-trailers-get: func(f: future-trailers) -> option>; - listen-to-future-trailers: func(f: future-trailers) -> pollable; - - // The following block defines the `future-write-trailers-result` resource, - // which is returned when finishing an `outgoing-stream` and asychronously - // indicates the success or failure of writing the trailers. - type future-write-trailers-result = u32; - drop-future-write-trailers-result: func(f: future-write-trailers-result); - future-write-trailers-result-get: func(f: future-write-trailers-result) -> option>; - listen-to-future-write-trailers-result: func(f: future-write-trailers-result) -> pollable; - // The following block defines the `incoming-request` and `outgoing-request` // resource types that correspond to HTTP standard Requests. Soon, when - // resource types are added, the `u32` type aliases can be replaced by proper - // `resource` type definitions containing all the functions as methods. - // Later, Preview2 will allow both types to be merged together into a single - // `request` type (that uses the single `stream` type mentioned above). The - // `consume` and `write` methods may only be called once (and return failure - // thereafter). The `headers` and `trailers` passed into and out of requests - // are shared with the request, with all mutations visible to all uses. - // Components MUST avoid updating `headers` and `trailers` after passing a - // request that points to them to the outside world. - // The streams returned by `consume` and `write` are owned by the request and - // response objects. The streams are destroyed when the request/response is - // dropped, thus a client MUST drop any handle referring to a request/response stream - // before dropping the request/response or passing ownership of the request/response - // to the outside world. The caller can also call drop on the stream before the - // request/response is dropped if they want to release resources earlier. - type incoming-request = u32; - type outgoing-request = u32; - drop-incoming-request: func(request: incoming-request); - drop-outgoing-request: func(request: outgoing-request); - incoming-request-method: func(request: incoming-request) -> method; - incoming-request-path-with-query: func(request: incoming-request) -> option; - incoming-request-scheme: func(request: incoming-request) -> option; - incoming-request-authority: func(request: incoming-request) -> option; - incoming-request-headers: func(request: incoming-request) -> headers; - incoming-request-consume: func(request: incoming-request) -> result; - new-outgoing-request: func( - method: method, - path-with-query: option, - scheme: option, - authority: option, - headers: headers - ) -> result; - outgoing-request-write: func(request: outgoing-request) -> result; + // resource types are added, the `u32` type aliases can be replaced by + // proper `resource` type definitions containing all the functions as + // methods. Later, Preview2 will allow both types to be merged together into + // a single `request` type (that uses the single `stream` type mentioned + // above). The `consume` and `write` methods may only be called once (and + // return failure thereafter). + resource incoming-request { + method: func() -> method; + + path-with-query: func() -> option; + + scheme: func() -> option; + + authority: func() -> option; + + headers: func() -> headers; + + // Will return the incoming-body child at most once. If called more than + // once, subsequent calls will return error. + consume: func() -> result; + } + + resource outgoing-request { + constructor( + method: method, + path-with-query: option, + scheme: option, + authority: option, + headers: borrow + ); + + // Will return the outgoing-body child at most once. If called more than + // once, subsequent calls will return error. + write: func() -> result; + } // Additional optional parameters that can be set when making a request. record request-options { // The following timeouts are specific to the HTTP protocol and work - // independently of the overall timeouts passed to `io.poll.poll-oneoff`. + // independently of the overall timeouts passed to `io.poll.poll-list`. // The timeout for the initial connect. connect-timeout-ms: option, @@ -143,9 +126,9 @@ interface types { // definition. Later, with Preview3, the need for an outparam goes away entirely // (the `wasi:http/handler` interface used for both incoming and outgoing can // simply return a `stream`). - type response-outparam = u32; - drop-response-outparam: func(response: response-outparam); - set-response-outparam: func(param: response-outparam, response: result) -> result; + resource response-outparam { + set: static func(param: response-outparam, response: result); + } // This type corresponds to the HTTP standard Status Code. type status-code = u16; @@ -157,32 +140,74 @@ interface types { // Preview2 will allow both types to be merged together into a single `response` // type (that uses the single `stream` type mentioned above). The `consume` and // `write` methods may only be called once (and return failure thereafter). - // The `headers` and `trailers` passed into and out of responses are shared - // with the response, with all mutations visible to all uses. Components MUST - // avoid updating `headers` and `trailers` after passing a response that - // points to them to the outside world. - type incoming-response = u32; - type outgoing-response = u32; - drop-incoming-response: func(response: incoming-response); - drop-outgoing-response: func(response: outgoing-response); - incoming-response-status: func(response: incoming-response) -> status-code; - incoming-response-headers: func(response: incoming-response) -> headers; - incoming-response-consume: func(response: incoming-response) -> result; - new-outgoing-response: func( - status-code: status-code, - headers: headers - ) -> result; - outgoing-response-write: func(response: outgoing-response) -> result; + resource incoming-response { + status: func() -> status-code; - // The following block defines a special resource type used by the - // `wasi:http/outgoing-handler` interface to emulate - // `future>` in advance of Preview3. Given a - // `future-incoming-response`, the client can call the non-blocking `get` - // method to get the result if it is available. If the result is not available, - // the client can call `listen` to get a `pollable` that can be passed to - // `io.poll.poll-oneoff`. - type future-incoming-response = u32; - drop-future-incoming-response: func(f: future-incoming-response); - future-incoming-response-get: func(f: future-incoming-response) -> option>; - listen-to-future-incoming-response: func(f: future-incoming-response) -> pollable; + headers: func() -> headers; + + // May be called at most once. returns error if called additional times. + // TODO: make incoming-request-consume work the same way, giving a child + // incoming-body. + consume: func() -> result; + } + + resource incoming-body { + // returned input-stream is a child - the implementation may trap if + // incoming-body is dropped (or consumed by call to + // incoming-body-finish) before the input-stream is dropped. + // May be called at most once. returns error if called additional times. + %stream: func() -> result; + + // takes ownership of incoming-body. this will trap if the + // incoming-body-stream child is still alive! + finish: static func(this: incoming-body) -> future-trailers; + } + + resource future-trailers { + /// Pollable that resolves when the body has been fully read, and the trailers + /// are ready to be consumed. + subscribe: func() -> pollable; + + /// Retrieve reference to trailers, if they are ready. + get: func() -> option>; + } + + resource outgoing-response { + constructor(status-code: status-code, headers: borrow); + + /// Will give the child outgoing-response at most once. subsequent calls will + /// return an error. + write: func() -> result; + } + + resource outgoing-body { + /// Will give the child output-stream at most once. subsequent calls will + /// return an error. + write: func() -> result; + + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` is + /// dropped without calling `outgoing-body-finalize`, the implementation + /// should treat the body as corrupted. + finish: static func(this: outgoing-body, trailers: option); + } + + /// The following block defines a special resource type used by the + /// `wasi:http/outgoing-handler` interface to emulate + /// `future>` in advance of Preview3. Given a + /// `future-incoming-response`, the client can call the non-blocking `get` + /// method to get the result if it is available. If the result is not available, + /// the client can call `listen` to get a `pollable` that can be passed to + /// `wasi:io/poll.poll-list`. + resource future-incoming-response { + /// option indicates readiness. + /// outer result indicates you are allowed to get the + /// incoming-response-or-error at most once. subsequent calls after ready + /// will return an error here. + /// inner result indicates whether the incoming-response was available, or an + /// error occured. + get: func() -> option>>; + + subscribe: func() -> pollable; + } }