diff --git a/src/main/java/io/roastedroot/proxywasm/impl/Imports.java b/src/main/java/io/roastedroot/proxywasm/impl/Imports.java index 7c677ba..9532e23 100644 --- a/src/main/java/io/roastedroot/proxywasm/impl/Imports.java +++ b/src/main/java/io/roastedroot/proxywasm/impl/Imports.java @@ -6,6 +6,7 @@ import com.dylibso.chicory.experimental.hostmodule.annotations.WasmExport; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.runtime.WasmRuntimeException; +import io.roastedroot.proxywasm.v1.Action; import io.roastedroot.proxywasm.v1.BufferType; import io.roastedroot.proxywasm.v1.Handler; import io.roastedroot.proxywasm.v1.LogLevel; @@ -784,24 +785,49 @@ int proxyGetCurrentTimeNanoseconds(int returnTime) { } } + /** + * Resumes processing of paused stream_type. + * + * see: https://github.com/proxy-wasm/spec/tree/main/abi-versions/v0.2.1#proxy_continue_stream + */ @WasmExport int proxyContinueStream(int arg) { var streamType = StreamType.fromInt(arg); if (streamType == null) { return WasmResult.BAD_ARGUMENT.getValue(); } - switch (streamType) { - case REQUEST: - return handler.continueRequest().getValue(); - case RESPONSE: - return handler.continueResponse().getValue(); - case DOWNSTREAM: - return handler.continueDownstream().getValue(); - case UPSTREAM: - return handler.continueUpstream().getValue(); - } - // should never reach here - return WasmResult.INTERNAL_FAILURE.getValue(); + WasmResult result = handler.setAction(streamType, Action.CONTINUE); + return result.getValue(); + } + + /** + * Resumes processing of paused HTTP request. + * + * see: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.1.0/README.md#proxy_continue_request + */ + @WasmExport + void proxyContinueRequest() { + handler.setAction(StreamType.REQUEST, Action.CONTINUE); + } + + /** + * Resumes processing of paused HTTP response. + * + * see: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.1.0/README.md#proxy_continue_response + */ + @WasmExport + void proxyContinueResponse() { + handler.setAction(StreamType.RESPONSE, Action.CONTINUE); + } + + /** + * Clears cached HTTP route. + * + * see: https://github.com/proxy-wasm/spec/blob/main/abi-versions/v0.1.0/README.md#proxy_clear_route_cache + */ + @WasmExport + void proxyClearRouteCache() { + handler.clearRouteCache(); } @WasmExport diff --git a/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java b/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java index cd1f906..7216a8e 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java @@ -236,23 +236,13 @@ public WasmResult setHttpRequestBody(byte[] body) { } @Override - public WasmResult continueRequest() { - return next().continueRequest(); + public WasmResult setAction(StreamType streamType, Action action) { + return next().setAction(streamType, action); } @Override - public WasmResult continueResponse() { - return next().continueResponse(); - } - - @Override - public WasmResult continueDownstream() { - return next().continueDownstream(); - } - - @Override - public WasmResult continueUpstream() { - return next().continueUpstream(); + public WasmResult clearRouteCache() { + return next().clearRouteCache(); } @Override diff --git a/src/main/java/io/roastedroot/proxywasm/v1/Handler.java b/src/main/java/io/roastedroot/proxywasm/v1/Handler.java index 42fee6f..168f6e1 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/Handler.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/Handler.java @@ -371,19 +371,11 @@ default WasmResult setGrpcReceiveTrailerMetaData(Map metadata) { return WasmResult.UNIMPLEMENTED; } - default WasmResult continueRequest() { + default WasmResult setAction(StreamType streamType, Action action) { return WasmResult.UNIMPLEMENTED; } - default WasmResult continueResponse() { - return WasmResult.UNIMPLEMENTED; - } - - default WasmResult continueDownstream() { - return WasmResult.UNIMPLEMENTED; - } - - default WasmResult continueUpstream() { + default WasmResult clearRouteCache() { return WasmResult.UNIMPLEMENTED; } diff --git a/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java b/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java index eeccd24..a1ade0d 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java @@ -20,14 +20,18 @@ public Action callOnRequestHeaders(boolean endOfStream) { var headers = handler.getHttpRequestHeaders(); int result = proxyWasm.exports().proxyOnRequestHeaders(id, len(headers), endOfStream ? 1 : 0); - return Action.fromInt(result); + Action action = Action.fromInt(result); + handler.setAction(StreamType.REQUEST, action); + return action; } public Action callOnResponseHeaders(boolean endOfStream) { var headers = handler.getHttpResponseHeaders(); int result = proxyWasm.exports().proxyOnResponseHeaders(id, len(headers), endOfStream ? 1 : 0); - return Action.fromInt(result); + Action action = Action.fromInt(result); + handler.setAction(StreamType.RESPONSE, action); + return action; } public Action callOnRequestBody(boolean endOfStream) { @@ -36,7 +40,9 @@ public Action callOnRequestBody(boolean endOfStream) { proxyWasm .exports() .proxyOnRequestBody(id, Helpers.len(requestBody), endOfStream ? 1 : 0); - return Action.fromInt(result); + Action action = Action.fromInt(result); + handler.setAction(StreamType.REQUEST, action); + return action; } public Action callOnResponseBody(boolean endOfStream) { @@ -45,6 +51,8 @@ public Action callOnResponseBody(boolean endOfStream) { proxyWasm .exports() .proxyOnResponseBody(id, Helpers.len(responseBody), endOfStream ? 1 : 0); - return Action.fromInt(result); + Action action = Action.fromInt(result); + handler.setAction(StreamType.RESPONSE, action); + return action; } } diff --git a/src/test/go-examples/multiple_dispatches/README.md b/src/test/go-examples/multiple_dispatches/README.md new file mode 100644 index 0000000..c03a1d4 --- /dev/null +++ b/src/test/go-examples/multiple_dispatches/README.md @@ -0,0 +1,5 @@ +## Attribution + +This example originally came from: +https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/multiple_dispatches +``` diff --git a/src/test/go-examples/multiple_dispatches/go.mod b/src/test/go-examples/multiple_dispatches/go.mod new file mode 100644 index 0000000..2f9bc58 --- /dev/null +++ b/src/test/go-examples/multiple_dispatches/go.mod @@ -0,0 +1,5 @@ +module github.com/proxy-wasm/proxy-wasm-go-sdk/examples/multiple_dispatches + +go 1.24 + +require github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 diff --git a/src/test/go-examples/multiple_dispatches/go.sum b/src/test/go-examples/multiple_dispatches/go.sum new file mode 100644 index 0000000..3ddb896 --- /dev/null +++ b/src/test/go-examples/multiple_dispatches/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 h1:wTcK6gcyTKJMeDka69AMjZYvisdI8CBXzTEfZ+2pOxI= +github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924/go.mod h1:9mBRvh8I6Td6sg3CwEY+zGFE4DKaIoieCaca1kQnDBE= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/test/go-examples/multiple_dispatches/main.go b/src/test/go-examples/multiple_dispatches/main.go new file mode 100644 index 0000000..d0ce252 --- /dev/null +++ b/src/test/go-examples/multiple_dispatches/main.go @@ -0,0 +1,100 @@ +// Copyright 2020-2024 Tetrate +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "strconv" + + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" +) + +const clusterName = "httpbin" + +func main() {} +func init() { + proxywasm.SetVMContext(&vmContext{}) +} + +// vmContext implements types.VMContext. +type vmContext struct { + // Embed the default VM context here, + // so that we don't need to reimplement all the methods. + types.DefaultVMContext +} + +// NewPluginContext implements types.VMContext. +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{} +} + +// pluginContext implements types.PluginContext. +type pluginContext struct { + // Embed the default plugin context here, + // so that we don't need to reimplement all the methods. + types.DefaultPluginContext +} + +// NewHttpContext implements types.PluginContext. +func (*pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &httpContext{contextID: contextID} +} + +// httpContext implements types.HttpContext. +type httpContext struct { + // Embed the default http context here, + // so that we don't need to reimplement all the methods. + types.DefaultHttpContext + // contextID is the unique identifier assigned to each httpContext. + contextID uint32 + // pendingDispatchedRequest is the number of pending dispatched requests. + pendingDispatchedRequest int +} + +const totalDispatchNum = 10 + +// OnHttpResponseHeaders implements types.HttpContext. +func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action { + // On each request response, we dispatch the http calls `totalDispatchNum` times. + // Note: DispatchHttpCall is asynchronously processed, so each loop is non-blocking. + for i := 0; i < totalDispatchNum; i++ { + if _, err := proxywasm.DispatchHttpCall(clusterName, [][2]string{ + {":path", "/"}, + {":method", "GET"}, + {":authority", ""}}, + nil, nil, 50000, ctx.dispatchCallback); err != nil { + panic(err) + } + // Now we have made a dispatched request, so we record it. + ctx.pendingDispatchedRequest++ + } + return types.ActionPause +} + +// dispatchCallback is the callback function called in response to the response arrival from the dispatched request. +func (ctx *httpContext) dispatchCallback(numHeaders, bodySize, numTrailers int) { + // Decrement the pending request counter. + ctx.pendingDispatchedRequest-- + if ctx.pendingDispatchedRequest == 0 { + // This case, all the dispatched request was processed. + // Adds a response header to the original response. + _ = proxywasm.AddHttpResponseHeader("total-dispatched", strconv.Itoa(totalDispatchNum)) + // And then contniue the original reponse. + _ = proxywasm.ResumeHttpResponse() + proxywasm.LogInfof("response resumed after processed %d dispatched request", totalDispatchNum) + } else { + proxywasm.LogInfof("pending dispatched requests: %d", ctx.pendingDispatchedRequest) + } +} diff --git a/src/test/go-examples/multiple_dispatches/main.wasm b/src/test/go-examples/multiple_dispatches/main.wasm new file mode 100644 index 0000000..ed40c11 Binary files /dev/null and b/src/test/go-examples/multiple_dispatches/main.wasm differ diff --git a/src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java b/src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java index f6c8d89..69c30d6 100644 --- a/src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java +++ b/src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java @@ -15,6 +15,83 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +// This file what the go code was: +// +// func TestOnHTTPRequestHeaders(t *testing.T) { +// type testCase struct { +// contentType string +// expectedAction types.Action +// } +// +// vmTest(t, func(t *testing.T, vm types.VMContext) { +// for name, tCase := range map[string]testCase{ +// "fails due to unsupported content type": { +// contentType: "text/html", +// expectedAction: types.ActionPause, +// }, +// "success for JSON": { +// contentType: "application/json", +// expectedAction: types.ActionContinue, +// }, +// } { +// t.Run(name, func(t *testing.T) { +// opt := proxytest.NewEmulatorOption().WithVMContext(vm) +// host, reset := proxytest.NewHostEmulator(opt) +// defer reset() +// +// require.Equal(t, types.OnPluginStartStatusOK, host.StartPlugin()) +// +// id := host.InitializeHttpContext() +// +// hs := [][2]string{{"content-type", tCase.contentType}} +// +// action := host.CallOnRequestHeaders(id, hs, false) +// assert.Equal(t, tCase.expectedAction, action) +// }) +// } +// }) +// } +// +// func TestOnHTTPRequestBody(t *testing.T) { +// type testCase struct { +// body string +// expectedAction types.Action +// } +// +// vmTest(t, func(t *testing.T, vm types.VMContext) { +// +// for name, tCase := range map[string]testCase{ +// "pauses due to invalid payload": { +// body: "invalid_payload", +// expectedAction: types.ActionPause, +// }, +// "pauses due to unknown keys": { +// body: `{"unknown_key":"unknown_value"}`, +// expectedAction: types.ActionPause, +// }, +// "success": { +// body: "{\"my_key\":\"my_value\"}", +// expectedAction: types.ActionContinue, +// }, +// } { +// t.Run(name, func(t *testing.T) { +// opt := proxytest. +// NewEmulatorOption(). +// WithPluginConfiguration([]byte(`{"requiredKeys": ["my_key"]}`)). +// WithVMContext(vm) +// host, reset := proxytest.NewHostEmulator(opt) +// defer reset() +// +// require.Equal(t, types.OnPluginStartStatusOK, host.StartPlugin()) +// +// id := host.InitializeHttpContext() +// +// action := host.CallOnRequestBody(id, []byte(tCase.body), true) +// assert.Equal(t, tCase.expectedAction, action) +// }) +// } +// }) +// } /** * Java port of https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/json_validation/main_test.go */ diff --git a/src/test/java/io/roastedroot/proxywasm/MockHandler.java b/src/test/java/io/roastedroot/proxywasm/MockHandler.java index 0677e1a..4d64a6a 100644 --- a/src/test/java/io/roastedroot/proxywasm/MockHandler.java +++ b/src/test/java/io/roastedroot/proxywasm/MockHandler.java @@ -4,10 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.roastedroot.proxywasm.v1.Action; import io.roastedroot.proxywasm.v1.Handler; import io.roastedroot.proxywasm.v1.Helpers; import io.roastedroot.proxywasm.v1.LogLevel; import io.roastedroot.proxywasm.v1.MetricType; +import io.roastedroot.proxywasm.v1.StreamType; import io.roastedroot.proxywasm.v1.WasmException; import io.roastedroot.proxywasm.v1.WasmResult; import java.util.ArrayList; @@ -421,4 +423,16 @@ public WasmResult removeMetric(int metricId) { metricsByName.remove(metric.name); return WasmResult.OK; } + + private Action action; + + @Override + public WasmResult setAction(StreamType streamType, Action action) { + this.action = action; + return WasmResult.OK; + } + + public Action getAction() { + return action; + } } diff --git a/src/test/java/io/roastedroot/proxywasm/MultipleDispatchesTest.java b/src/test/java/io/roastedroot/proxywasm/MultipleDispatchesTest.java new file mode 100644 index 0000000..37ee2fc --- /dev/null +++ b/src/test/java/io/roastedroot/proxywasm/MultipleDispatchesTest.java @@ -0,0 +1,65 @@ +package io.roastedroot.proxywasm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.dylibso.chicory.wasm.Parser; +import io.roastedroot.proxywasm.v1.Action; +import io.roastedroot.proxywasm.v1.ProxyWasm; +import io.roastedroot.proxywasm.v1.StartException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Java port of https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/master/examples/multiple_dispatches/main_test.go + */ +public class MultipleDispatchesTest { + private final MockHandler handler = new MockHandler(); + + @Test + public void testHttpContextOnHttpRequestHeaders() throws StartException { + // Load the WASM module + var module = Parser.parse(Path.of("./src/test/go-examples/multiple_dispatches/main.wasm")); + + // Create and configure the ProxyWasm instance + try (var host = ProxyWasm.builder().withPluginHandler(handler).build(module)) { + + // Create an HTTP context + try (var context = host.createHttpContext(handler)) { + // Set response headers and call OnResponseHeaders + Map headers = Map.of("key", "value"); + handler.setHttpResponseHeaders(headers); + Action action = context.callOnResponseHeaders(false); + + // Verify the action is PAUSE + assertEquals(Action.PAUSE, action, "Expected PAUSE action for response headers"); + + // Verify DispatchHttpCall is called 10 times + var httpCalls = handler.getHttpCalls(); + assertEquals(10, httpCalls.size(), "Expected 10 HTTP calls to be dispatched"); + + assertEquals(Action.PAUSE, handler.getAction()); + + // Emulate Envoy receiving all responses to the dispatched callouts + for (var entry : httpCalls.entrySet()) { + var callout = entry.getValue(); + host.sendHttpCallResponse( + callout.id, + new HashMap<>(), // headers + new HashMap<>(), // trailers + new byte[0] // body + ); + } + + assertEquals(Action.CONTINUE, handler.getAction()); + + // Check logs for expected messages + handler.assertLogsContain( + "pending dispatched requests: 9", + "pending dispatched requests: 1", + "response resumed after processed 10 dispatched request"); + } + } + } +}