Skip to content

Commit 9cecd03

Browse files
Allows async hosts by splitting "handle" (#35)
This reduces the complexity of implementing async hosts such as mosn by splitting the guest export "handle" into "handle_request" and "handle_response". * `handle_request`: called by the host on each request and returns `next=1` to continue to the next handler on the host. * `handle_response`: caller by the host, regardless of error, when `handle_request` returned `next=1`. The `next` field is combined with an optional `reqCtx` as the i64 result of `handler_request`. When the guest decides to proceed to the next handler, it can return `ctxNext=1` which is the same as `next=1` without any request context. If it wants the host to propagate request context, it shifts that into the upper 32-bits of ctxNext like so: ```go ctxNext = uint64(reqCtx) << 32 if next { ctxNext |= 1 } ``` == Impact of the new design == This results in the same amount of calls between the guest and the host, as "next" is no longer needed. This also allows the guest to be called regardless of host failure, for example in logging interceptors. On the other hand, splitting the ABI may feel a little more work, as you have to define two callbacks, not one. That said, in TinyGo, the default response callback will be invisible to those who only deal with requests, as it can simply no-op return. Signed-off-by: Adrian Cole <[email protected]>
1 parent 11329b5 commit 9cecd03

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+484
-222
lines changed

api/handler/handler.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type Middleware[H any] interface {
2323
}
2424

2525
// Host supports the host side of the WebAssembly module named HostModule.
26-
// These callbacks are used by the guest function export FuncHandle.
26+
// These callbacks are used by the guest function export FuncHandleRequest.
2727
type Host interface {
2828
// EnableFeatures supports the WebAssembly function export EnableFeatures.
2929
EnableFeatures(ctx context.Context, features Features) Features
@@ -99,10 +99,6 @@ type Host interface {
9999
// FeatureTrailers is not supported.
100100
RemoveRequestTrailer(ctx context.Context, name string)
101101

102-
// Next supports the WebAssembly function export FuncNext, which invokes
103-
// the next handler.
104-
Next(ctx context.Context)
105-
106102
// GetStatusCode supports the WebAssembly function export
107103
// FuncGetStatusCode.
108104
GetStatusCode(ctx context.Context) uint32

api/handler/wasm.go

+63-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
package handler
22

3+
// CtxNext is the result of FuncHandleRequest. For compatability with
4+
// WebAssembly Core Specification 1.0, two uint32 values are combined into a
5+
// single uint64 in the following order:
6+
//
7+
// - ctx: opaque 32-bits the guest defines and the host propagates to
8+
// FuncHandleResponse. A typical use is correlation of request state.
9+
// - next: one to proceed to the next handler on the host. zero to skip any
10+
// next handler. Guests skip when they wrote a response or decided not to.
11+
//
12+
// When the guest decides to proceed to the next handler, it can return
13+
// `ctxNext=1` which is the same as `next=1` without any request context. If it
14+
// wants the host to propagate request context, it shifts that into the upper
15+
// 32-bits of ctxNext like so:
16+
//
17+
// ctxNext = uint64(reqCtx) << 32
18+
// if next {
19+
// ctxNext |= 1
20+
// }
21+
//
22+
// # Examples
23+
//
24+
// - 0<<32|0 (0): don't proceed to the next handler.
25+
// - 0<<32|1 (1): proceed to the next handler without context state.
26+
// - 16<<32|1 (68719476737): proceed to the next handler and call
27+
// FuncHandleResponse with 16.
28+
// - 16<<32|16 (68719476736): the value 16 is ignored because
29+
// FuncHandleResponse won't be called.
30+
type CtxNext uint64
31+
332
// BufLimit is the possibly zero maximum length of a result value to write in
433
// bytes. If the actual value is larger than this, nothing is written to
534
// memory.
@@ -60,7 +89,7 @@ const (
6089
//
6190
// # Notes on FuncWriteBody
6291
//
63-
// The first call to FuncWriteBody in FuncHandle overwrites any request
92+
// The first call to FuncWriteBody in FuncHandleRequest overwrites any request
6493
// body.
6594
BodyKindRequest BodyKind = 0
6695

@@ -74,7 +103,7 @@ const (
74103
//
75104
// # Notes on FuncWriteBody
76105
//
77-
// The first call to FuncWriteBody in FuncHandle or after FuncNext
106+
// The first call to FuncWriteBody in FuncHandleRequest or after FuncNext
78107
// overwrites any response body.
79108
BodyKindResponse BodyKind = 1
80109
)
@@ -116,14 +145,14 @@ const (
116145
HostModule = "http_handler"
117146

118147
// FuncEnableFeatures tries to enable the given features and returns the
119-
// Features bitflag supported by the host. This must be called prior to
120-
// FuncNext to enable Features needed by the guest.
148+
// Features bitflag supported by the host. To have any affect, this must be
149+
// called prior to returning from FuncHandleRequest.
121150
//
122-
// This may be called prior to FuncHandle, for example inside a start
123-
// function. Doing so reduces overhead per-call and also allows the guest
124-
// to fail early on unsupported.
151+
// This may be called prior to FuncHandleRequest, for example inside a
152+
// start function. Doing so reduces overhead per-call and also allows the
153+
// guest to fail early on unsupported.
125154
//
126-
// If called during FuncHandle, any new features are only enabled for the
155+
// If called during FuncHandleRequest, any new features are enabled for the
127156
// scope of the current request. This allows fine-grained access to
128157
// expensive features such as FeatureBufferResponse.
129158
//
@@ -153,11 +182,34 @@ const (
153182
// See https://github.com/http-wasm/http-wasm-abi/blob/main/http_handler/http_handler.wit.md#log
154183
FuncLog = "log"
155184

156-
// FuncHandle is the entrypoint guest export called by the host when
185+
// FuncHandleRequest is the entrypoint guest export called by the host when
157186
// processing a request.
158187
//
159-
// See https://github.com/http-wasm/http-wasm-abi/blob/main/http_handler/http_handler.wit.md#handle
160-
FuncHandle = "handle"
188+
// To proceed to the next handler, the guest returns CtxNext `next=1`. The
189+
// simplest result is uint64(1). Guests who need request correlation can
190+
// also set the CtxNext "ctx" field. This will be propagated by the host as
191+
// the `reqCtx` parameter of FuncHandleResponse.
192+
//
193+
// To skip any next handler, the guest returns CtxNext `next=0`, or simply
194+
// uint64(0). In this case, FuncHandleResponse will not be called.
195+
FuncHandleRequest = "handle_request"
196+
197+
// FuncHandleResponse is called by the host after processing the next
198+
// handler when the guest returns CtxNext `next=1` from FuncHandleRequest.
199+
//
200+
// The `reqCtx` parameter is a possibly zero CtxNext "ctx" field the host
201+
// the host propagated from FuncHandleRequest. This allows request
202+
// correlation for guests who need it.
203+
//
204+
// The `isError` parameter is one if there was a host error producing a
205+
// response. This allows guests to clean up any resources.
206+
//
207+
// By default, whether the next handler flushes the response prior to
208+
// returning is implementation-specific. If your handler needs to inspect
209+
// or manipulate the downstream response, enable FeatureBufferResponse via
210+
// FuncEnableFeatures prior to returning from FuncHandleRequest.
211+
// TODO: update
212+
FuncHandleResponse = "handle_response"
161213

162214
// FuncGetMethod writes the method to memory if it isn't larger than
163215
// BufLimit. The result is its length in bytes. Ex. "GET"
@@ -248,18 +300,6 @@ const (
248300
// TODO: document on http-wasm-abi
249301
FuncWriteBody = "write_body"
250302

251-
// FuncNext calls a downstream handler and blocks until it is finished
252-
// processing.
253-
//
254-
// By default, whether the next handler flushes the response prior to
255-
// returning is implementation-specific. If your handler needs to inspect
256-
// or manipulate the downstream response, enable FeatureBufferResponse via
257-
// FuncEnableFeatures prior to calling this function.
258-
//
259-
// TODO: update existing document on http-wasm-abi
260-
// See https://github.com/http-wasm/http-wasm-abi/blob/main/http_handler/http_handler.wit.md#next
261-
FuncNext = "next"
262-
263303
// FuncGetStatusCode returns the status code produced by FuncNext. This
264304
// requires FeatureBufferResponse.
265305
//

examples/auth.wasm

43 Bytes
Binary file not shown.

examples/auth.wat

+15-9
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
(param $buf i32) (param $buf_limit i32)
1313
(result (; count << 32| len ;) i64)))
1414

15-
;; next dispatches control to the next handler on the host.
16-
(import "http_handler" "next" (func $next))
17-
1815
;; set_header_value overwrites a header of the given $kind and $name with a
1916
;; single value.
2017
(import "http_handler" "set_header_value" (func $set_header_value
@@ -68,13 +65,15 @@
6865
(global.get $authenticate_value)
6966
(global.get $authenticate_value_len)))
7067

71-
;; handle tries BASIC authentication and dispatches to "next" or returns 401.
72-
(func $handle (export "handle")
68+
;; handle_request tries BASIC authentication and dispatches to the next
69+
;; handler or returns 401.
70+
(func (export "handle_request") (result (; ctx_next ;) i64)
7371

7472
(local $result i64)
7573
(local $count i32)
7674
(local $len i32)
7775
(local $authorization_eq i32)
76+
(local $ctx_next i64)
7877

7978
(local.set $result (call $get_authorization))
8079

@@ -86,15 +85,15 @@
8685
(then ;; multiple headers, invalid
8786
(call $set_authenticate)
8887
(call $set_status_code (i32.const 401))
89-
(return)))
88+
(return (i64.const 0)))) ;; don't call the next handler
9089

9190
;; len = uint32(result)
9291
(local.set $len (i32.wrap_i64 (local.get $result)))
9392

9493
(if (i32.ne (global.get $authorization_values_len) (local.get $len))
9594
(then ;; authorization_values_length != i32($header_value)
9695
(call $set_status_code (i32.const 401))
97-
(return)))
96+
(return (i64.const 0)))) ;; don't call the next handler
9897

9998
(local.set $authorization_eq (call $memeq
10099
(global.get $buf)
@@ -104,8 +103,15 @@
104103
(if (i32.eqz (local.get $authorization_eq))
105104
(then ;; authenticate_value != authorization_values
106105
(call $set_status_code (i32.const 401)))
107-
(else ;; authorization passed! call the next handler
108-
(call $next))))
106+
(else ;; authorization passed!
107+
(local.set $ctx_next (i64.const 1)))) ;; call the next handler
108+
(return (local.get $ctx_next))
109+
110+
;; uint32(ctx_next) == 1 means proceed to the next handler on the host.
111+
(return (i64.const 1)))
112+
113+
;; handle_response is no-op as this is a request-only handler.
114+
(func (export "handle_response") (param $reqCtx i32) (param $is_error i32))
109115

110116
;; memeq is like memcmp except it returns 0 (ne) or 1 (eq)
111117
(func $memeq (param $ptr1 i32) (param $ptr2 i32) (param $len i32) (result i32)

examples/config.wasm

32 Bytes
Binary file not shown.

examples/config.wat

+6-4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
(param $buf i32) (param $buf_limit i32)
1515
(result (; len ;) i32)))
1616

17-
;; next dispatches control to the next handler on the host.
18-
(import "http_handler" "next" (func $next))
17+
;; handle_request just calls next by returning non-zero.
18+
(func (export "handle_request") (result (; ctx_next ;) i64)
19+
;; uint32(ctx_next) == 1 means proceed to the next handler on the host.
20+
(return (i64.const 1)))
1921

20-
;; handle just calls next.
21-
(func (export "handle") (call $next))
22+
;; handle_response is no-op as this is a request-only handler.
23+
(func (export "handle_response") (param $reqCtx i32) (param $is_error i32))
2224

2325
;; http_handler guests are required to export "memory", so that imported
2426
;; functions like "get_header" can read memory.

examples/log.wasm

51 Bytes
Binary file not shown.

examples/log.wat

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
(data (i32.const 0) "hello world")
2020
(global $message_len i32 (i32.const 11))
2121

22-
(func $handle (export "handle")
22+
(func (export "handle_request") (result (; ctx_next ;) i64)
2323
;; We expect debug logging to be disabled. Panic otherwise!
2424
(if (i32.eq
2525
(call $log_enabled (i32.const -1)) ;; log_level_debug
@@ -29,5 +29,11 @@
2929
(call $log
3030
(i32.const 0) ;; log_level_info
3131
(global.get $message)
32-
(global.get $message_len)))
32+
(global.get $message_len))
33+
34+
;; uint32(ctx_next) == 1 means proceed to the next handler on the host.
35+
(return (i64.const 1)))
36+
37+
;; handle_response is no-op as this is a request-only handler.
38+
(func (export "handle_response") (param $reqCtx i32) (param $is_error i32))
3339
)

examples/redact.wasm

39 Bytes
Binary file not shown.

examples/redact.wat

+11-6
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@
3030
(param $kind i32)
3131
(param $buf i32) (param $buf_len i32)))
3232

33-
;; next dispatches control to the next handler on the host.
34-
(import "http_handler" "next" (func $next))
35-
3633
;; http_handler guests are required to export "memory", so that imported
3734
;; functions like $read_body can read memory.
3835
(memory (export "memory") 1 1 (; 1 page==64KB ;))
@@ -117,8 +114,8 @@
117114

118115
(local.get $len))
119116

120-
;; handle implements a simple HTTP router.
121-
(func $handle (export "handle")
117+
;; handle_request redacts any request body.
118+
(func (export "handle_request") (result (; ctx_next ;) i64)
122119
(local $len i32)
123120

124121
;; load the request body from the upstream handler into memory.
@@ -131,7 +128,15 @@
131128
(i32.const 0) ;; body_kind_request
132129
(global.get $body) (local.get $len))))
133130

134-
(call $next) ;; dispatch with $feature_buffer_response enabled.
131+
;; uint32(ctx_next) == 1 means proceed to the next handler on the host.
132+
(return (i64.const 1)))
133+
134+
;; handle_response redacts any request body.
135+
(func (export "handle_response") (param $reqCtx i32) (param $is_error i32)
136+
(local $len i32)
137+
138+
(if (i32.eq (local.get $is_error) (i32.const 1))
139+
(then (return))) ;; nothing to redact on error
135140

136141
;; load the response body from the downstream handler into memory.
137142
(local.set $len (call $must_read_body (i32.const 1)))

examples/router.wasm

18 Bytes
Binary file not shown.

examples/router.wat

+9-10
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
(import "http_handler" "set_uri" (func $set_uri
1313
(param $uri i32) (param $uri_len i32)))
1414

15-
;; next dispatches control to the next handler on the host.
16-
(import "http_handler" "next" (func $next))
17-
1815
;; set_header_value overwrites a header of the given $kind and $name with a
1916
;; single value.
2017
(import "http_handler" "set_header_value" (func $set_header_value
@@ -52,8 +49,8 @@
5249
(data (i32.const 96) "hello world")
5350
(global $body_len i32 (i32.const 11))
5451

55-
;; handle implements a simple HTTP router.
56-
(func $handle (export "handle")
52+
;; handle_request implements a simple HTTP router.
53+
(func (export "handle_request") (result (; ctx_next ;) i64)
5754

5855
(local $uri_len i32)
5956

@@ -66,8 +63,7 @@
6663
;; if uri_len > uri_limit { next() }
6764
(if (i32.gt_u (local.get $uri_len) (global.get $uri_limit))
6865
(then
69-
(call $next)
70-
(return))) ;; dispatch if the uri is too long.
66+
(return (i64.const 1)))) ;; dispatch if the uri is too long.
7167

7268
;; Next, strip any paths starting with '/host' and dispatch.
7369

@@ -83,8 +79,7 @@
8379
(call $set_uri ;; uri = uri[path_prefix_len:]
8480
(i32.add (global.get $uri) (global.get $path_prefix_len))
8581
(i32.sub (local.get $uri_len) (global.get $path_prefix_len)))
86-
(call $next)
87-
(return))))) ;; dispatch with the stripped path.
82+
(return (i64.const 1)))))) ;; dispatch with the stripped path.
8883

8984
;; Otherwise, serve a static response.
9085
(call $set_header_value
@@ -96,7 +91,11 @@
9691
(call $write_body
9792
(i32.const 1) ;; body_kind_response
9893
(global.get $body)
99-
(global.get $body_len)))
94+
(global.get $body_len))
95+
(return (i64.const 0))) ;; don't call the next handler
96+
97+
;; handle_response is no-op as this is a request-only handler.
98+
(func (export "handle_response") (param $reqCtx i32) (param $is_error i32))
10099

101100
;; memeq is like memcmp except it returns 0 (ne) or 1 (eq)
102101
(func $memeq (param $ptr1 i32) (param $ptr2 i32) (param $len i32) (result i32)

examples/wasi.wasm

81 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)