This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 93cff09692 [ISSUE #7563] Document wasm host abi and test a gateway
reference plugin. (#7567)
93cff09692 is described below
commit 93cff096923b2b9041edcd89111e19899923a5de
Author: redyyang <[email protected]>
AuthorDate: Tue Oct 13 11:13:40 2026 +0800
[ISSUE #7563] Document wasm host abi and test a gateway reference plugin.
(#7567)
* test(plugin-wasm-base): document host abi and add gateway reference tests.
* fix(ci): use recognizable license headers in wasm gateway fixtures.
---------
Co-authored-by: redyyang <[email protected]>
---
.github/scripts/verify-wasm-fixtures.py | 106 ++++++
.github/workflows/wasm-fixtures.yml | 64 ++++
shenyu-plugin/shenyu-plugin-wasm-api/ABI-v1.md | 243 ++++++++++++++
.../shenyu/plugin/wasm/api/loader/WasmLoader.java | 26 +-
.../shenyu-plugin-wasm-api/src/test/README.md | 30 +-
shenyu-plugin/shenyu-plugin-wasm-base/README.md | 112 +++++++
shenyu-plugin/shenyu-plugin-wasm-base/pom.xml | 30 ++
.../wasm/base/gateway/ReferenceWasmPlugin.java | 179 ++++++++++
.../gateway/ReferenceWasmPluginDataHandler.java | 130 ++++++++
.../plugin/wasm/base/gateway/WasmGatewayTest.java | 364 +++++++++++++++++++++
.../base/gateway/WasmLifecycleContractTest.java | 99 ++++++
...ugin.wasm.base.gateway.ReferenceWasmPlugin.wasm | Bin 0 -> 358 bytes
...ase.gateway.ReferenceWasmPluginDataHandler.wasm | Bin 0 -> 293 bytes
...y.WasmGatewayTest$IncompatibleResultPlugin.wasm | Bin 0 -> 107 bytes
...ateway.WasmGatewayTest$InvalidMemoryPlugin.wasm | Bin 0 -> 95 bytes
...teway.WasmGatewayTest$MissingExecutePlugin.wasm | Bin 0 -> 25 bytes
...ateway.WasmGatewayTest$MissingHooksHandler.wasm | Bin 0 -> 25 bytes
...ase.gateway.WasmGatewayTest$TrappingPlugin.wasm | Bin 0 -> 94 bytes
...asmLifecycleContractTest$InvalidHookLoader.wasm | Bin 0 -> 42 bytes
...cycleContractTest$InvalidInitializerLoader.wasm | Bin 0 -> 47 bytes
...ecycleContractTest$MissingDiscoveryHandler.wasm | Bin 0 -> 25 bytes
...fecycleContractTest$MissingMetadataHandler.wasm | Bin 0 -> 25 bytes
.../src/test/wasm-gateway/Makefile | 36 ++
.../test/wasm-gateway/configuration-handler.wat | 34 ++
.../src/test/wasm-gateway/incompatible-result.wat | 24 ++
.../src/test/wasm-gateway/invalid-hook.wat | 20 ++
.../src/test/wasm-gateway/invalid-initializer.wat | 20 ++
.../src/test/wasm-gateway/invalid-memory.wat | 22 ++
.../src/test/wasm-gateway/missing-hooks.wat | 18 +
.../src/test/wasm-gateway/reference-plugin.wat | 72 ++++
.../src/test/wasm-gateway/trap.wat | 23 ++
31 files changed, 1638 insertions(+), 14 deletions(-)
diff --git a/.github/scripts/verify-wasm-fixtures.py
b/.github/scripts/verify-wasm-fixtures.py
new file mode 100644
index 0000000000..9e8cd729a5
--- /dev/null
+++ b/.github/scripts/verify-wasm-fixtures.py
@@ -0,0 +1,106 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+"""Rebuild WAT fixtures exactly and test freshly compiled legacy Rust
guests."""
+
+import argparse
+import shutil
+import subprocess
+import tempfile
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+PLUGIN = ROOT / "shenyu-plugin"
+API = PLUGIN / "shenyu-plugin-wasm-api" / "src/test"
+BASE = PLUGIN / "shenyu-plugin-wasm-base" / "src/test"
+RUST_TOOLCHAIN = "1.90.0"
+RUST_GUESTS = [
+ (API, "rust-wasm-plugin",
"org.apache.shenyu.plugin.wasm.api.AbstractWasmPluginTest$RustWasmPlugin.wasm"),
+ (BASE, "rust-shenyu-wasm-plugin",
"org.apache.shenyu.plugin.wasm.base.AbstractShenyuWasmPluginTest$TestShenyuWasmPlugin.wasm"),
+ (BASE, "rust-plugin-data-handler",
"org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmPluginDataHandlerTest$TestWasmPluginDataHandler.wasm"),
+ (BASE, "rust-meta-data-handler",
"org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmMetaDataHandlerTest$TestWasmMetaDataHandler.wasm"),
+ (BASE, "rust-discovery-handler",
"org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmDiscoveryHandlerTest$TestWasmPluginDiscoveryHandler.wasm"),
+]
+
+
+def run(command, **kwargs):
+ subprocess.run(command, check=True, **kwargs)
+
+
+def verify_wat():
+ version = subprocess.check_output(["wat2wasm", "--version"],
text=True).strip()
+ if version != "1.0.42":
+ raise RuntimeError("WAT verification requires WABT 1.0.42, found " +
version)
+ count = 0
+ for family in ["wasm-bridge", "wasm-gateway"]:
+ directory = BASE / family
+ if not directory.exists():
+ continue
+ with tempfile.TemporaryDirectory(prefix="shenyu-wat-") as temporary:
+ run(["make", "-C", str(directory), "build", "RESOURCES=" +
temporary])
+ binaries = list(Path(temporary).glob("*.wasm"))
+ if not binaries:
+ raise RuntimeError("No fixtures generated for " + family)
+ for rebuilt in binaries:
+ committed = BASE / "resources" / rebuilt.name
+ if rebuilt.read_bytes() != committed.read_bytes():
+ raise RuntimeError("WAT fixture differs from committed
binary: " + rebuilt.name)
+ count += 1
+ if not count:
+ raise RuntimeError("No WAT fixture families found")
+ print("Verified " + str(count) + " byte-identical WAT fixtures",
flush=True)
+
+
+def verify_rust():
+ originals = {}
+ try:
+ with tempfile.TemporaryDirectory(prefix="shenyu-rust-") as temporary:
+ for source_root, crate, resource_name in RUST_GUESTS:
+ source = source_root / crate
+ copied = Path(temporary) / crate
+ copied.mkdir()
+ shutil.copyfile(source / "Cargo.toml", copied / "Cargo.toml")
+ shutil.copytree(source / "src", copied / "src")
+ run(["cargo", "+" + RUST_TOOLCHAIN, "build",
"--manifest-path", str(copied / "Cargo.toml"),
+ "--target", "wasm32-wasip1", "--release", "--offline"])
+ binary = copied / "target/wasm32-wasip1/release" /
(crate.replace("-", "_") + ".wasm")
+ resource = source_root / "resources" / resource_name
+ originals[resource] = resource.read_bytes()
+ shutil.copyfile(binary, resource)
+ # Maven copies the freshly rebuilt guests before the tests load
them.
+ # Legacy compiler versions were not recorded, so assert behavior
instead of bytes.
+ run(["./mvnw", "-B", "-ntp", "-pl",
"shenyu-plugin/shenyu-plugin-wasm-base", "-am", "test",
+ "-Dtest=*Wasm*Test",
"-Dsurefire.failIfNoSpecifiedTests=false",
+ "-DskipRemoteResources=true", "-Djacoco.skip=true",
"-Dmaven.javadoc.skip=true"], cwd=ROOT)
+ finally:
+ for resource, original in originals.items():
+ resource.write_bytes(original)
+ print("Verified five rebuilt Rust guests through the WASM Java suites",
flush=True)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ mode = parser.add_mutually_exclusive_group()
+ mode.add_argument("--wat-only", action="store_true")
+ mode.add_argument("--rust-only", action="store_true")
+ options = parser.parse_args()
+ if not options.rust_only:
+ verify_wat()
+ if not options.wat_only:
+ verify_rust()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/workflows/wasm-fixtures.yml
b/.github/workflows/wasm-fixtures.yml
new file mode 100644
index 0000000000..0923070470
--- /dev/null
+++ b/.github/workflows/wasm-fixtures.yml
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+name: wasm-fixtures
+
+on:
+ pull_request:
+ paths:
+ - 'shenyu-plugin/shenyu-plugin-wasm-api/**'
+ - 'shenyu-plugin/shenyu-plugin-wasm-base/**'
+ - '.github/scripts/verify-wasm-fixtures.py'
+ - '.github/workflows/wasm-fixtures.yml'
+ - 'pom.xml'
+ push:
+ branches: [master]
+ paths:
+ - 'shenyu-plugin/shenyu-plugin-wasm-api/**'
+ - 'shenyu-plugin/shenyu-plugin-wasm-base/**'
+ - '.github/scripts/verify-wasm-fixtures.py'
+ - '.github/workflows/wasm-fixtures.yml'
+ - 'pom.xml'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number ||
github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ rebuild-and-test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./actions/setup-java-with-retry
+ with:
+ java-version: "17"
+ distribution: "temurin"
+ - name: Install pinned WABT
+ shell: bash
+ run: |
+ curl --fail --location --retry 3 \
+
https://github.com/WebAssembly/wabt/releases/download/1.0.42/wabt-1.0.42-linux-x64.tar.gz
\
+ --output "${RUNNER_TEMP}/wabt.tar.gz"
+ echo
"84895407a6bbb80e918f33b16b2fb2206021c150b6bc9ff6f761263a745ab131
${RUNNER_TEMP}/wabt.tar.gz" | sha256sum --check
+ tar -xzf "${RUNNER_TEMP}/wabt.tar.gz" -C "${RUNNER_TEMP}"
+ echo "${RUNNER_TEMP}/wabt-1.0.42/bin" >> "${GITHUB_PATH}"
+ - name: Install pinned Rust and WASI target
+ run: rustup toolchain install 1.90.0 --profile minimal --target
wasm32-wasip1
+ - name: Check WAT binaries and test rebuilt Rust guests
+ run: python3 .github/scripts/verify-wasm-fixtures.py
diff --git a/shenyu-plugin/shenyu-plugin-wasm-api/ABI-v1.md
b/shenyu-plugin/shenyu-plugin-wasm-api/ABI-v1.md
new file mode 100644
index 0000000000..b19989ed22
--- /dev/null
+++ b/shenyu-plugin/shenyu-plugin-wasm-api/ABI-v1.md
@@ -0,0 +1,243 @@
+# ShenYu WASM host ABI, version 1
+
+This is the versioned contract for `shenyu-plugin-wasm-api` and
+`shenyu-plugin-wasm-base` in ShenYu `2.7.2-SNAPSHOT` with Java 17 and Chicory
+1.7.3. **Host ABI v1** names the existing transport and lifecycle contract;
+**reference request profile v1** below names one concrete serialization and
+error-code policy. They are separate contracts: the abstract runtime does not
+serialize HTTP exchanges or configuration DTOs automatically.
+
+## Loading and initialization
+
+A Java subclass is still needed to choose the plugin name, order, argument IDs,
+serialization, host callbacks and chain behavior. There is no standalone
starter
+for these abstract libraries. Register a concrete `ShenyuPlugin` bean with the
+gateway; register its `PluginDataHandler` bean if configuration hooks are
needed.
+
+The resource name is the concrete Java class's fully qualified binary name plus
+`.wasm`, at the classpath root. For a nested class the name contains `$`, not a
+package directory or a dot between enclosing and nested names. Each loader owns
+one Chicory instance and its linear memory. It tries the runtime compiler first
+and falls back to the interpreter when compilation/instantiation fails.
+
+The loader registers `wasi_snapshot_preview1` imports, then the `shenyu`
imports,
+and instantiates with automatic start disabled. If `_initialize: () -> ()` is
+exported, it is called once during construction (needed by the TinyGo
fixtures).
+Neither `_start` nor `main` is called. Linear memory must be available. An
absent
+resource or unavailable memory prevents initialization; malformed modules,
+unresolved imports, invalid export types and initialization traps propagate as
+initialization failures rather than being treated as missing optional hooks.
+
+WASI stdout/stderr are attached to the Java process. Environment, arguments and
+filesystem access are not inherited. The loader overrides `proc_exit(i32)`:
+zero returns normally, while nonzero throws `ShenyuWasmException`. Returning
from
+this import is ShenYu-specific; guests must not assume it terminates execution.
+
+## Imports and memory ownership
+
+The namespace is exactly `shenyu` (case sensitive). Both imports have this Wasm
+signature, including the **i64 pointer** even on wasm32:
+
+```wat
+(import "shenyu" "get_args" (func (param i64 i64 i32) (result i32)))
+(import "shenyu" "put_result" (func (param i64 i64 i32) (result i32)))
+```
+
+| Parameter | Meaning |
+| --- | --- |
+| `argId: i64` | Opaque Java-side invocation ID; not an object address. |
+| `addr: i64` | Byte offset into the calling instance's linear memory. wasm32
guests widen their pointer to i64. |
+| `len: i32` | Buffer capacity for `get_args`, exact payload byte count for
`put_result`. |
+
+`get_args` lets the host write serialized arguments into guest-owned memory.
+Its conventional success result is the number of bytes written; zero means no
+data. `put_result` lets the host copy/process bytes from guest-owned memory;
+its conventional success result is zero, with nonzero indicating an adapter
+error. The guest must keep the referenced bytes valid until the import returns.
+The host must copy any bytes it needs after the callback. Lengths count bytes,
+not characters; strings need an explicitly agreed encoding and are not
+implicitly NUL-terminated.
+
+Validate nonnegative lengths and the complete `addr..addr+len` range before
+accessing memory. Do not silently truncate i64 addresses to Java int values.
+The reference adapter uses `Math.toIntExact` and Chicory's checked memory
access;
+it does not support offsets beyond Java's signed int range. Address zero is a
+valid memory offset, not a null pointer by itself. A zero-length payload needs
+an adapter policy; the runtime does not impose one.
+
+### Serialization, short buffers and codes
+
+`WasmLoader`'s built-in callbacks are **no-ops returning zero**. Override
+`onGetArgs`/`onPutResult` (for loader subclasses), or register imports with the
+`Consumer<Store>` constructor of `AbstractShenyuWasmPlugin`. A non-null
consumer
+replaces both built-in `shenyu` imports and skips `initWasmCallJavaFunc`; it
does
+not replace the WASI imports. Host callbacks can be registered during
+construction, so they must not require subclass fields before those fields have
+been initialized.
+
+Host ABI v1 has no universal JSON schema, short-buffer retry protocol or error
+number registry. Existing Rust/TinyGo unit adapters use small UTF-8 strings and
+may copy `min(capacity, payload size)`. Their behavior does not establish a
+lossless production protocol. Every concrete adapter must document its payload,
+short-buffer behavior, return codes and result validation. The reference
profile
+below specifies and tests all of these. Negative codes are ordinary i32 import
+results; the loader does not automatically turn them into exceptions or HTTP
+responses. A guest must check them. Likewise, Java callers do not interpret a
+lifecycle export's return value as a chain decision: use `put_result` and the
+Java continuation method.
+
+## Lifecycle exports
+
+All names are case sensitive. The fixtures use `(i64) -> ()` for ID-bearing
+exports and `() -> ()` for `refresh` and `_initialize`. Extra return values are
+not interpreted by the templates; adapters should use the documented
signatures.
+
+| Java abstraction | Export | Required when invoked | Missing-export behavior |
+| --- | --- | --- | --- |
+| `AbstractWasmPlugin` | `execute(i64)` | Yes | HTTP 500, ShenYu result code
533 (`WASM_FUNC_NOT_FOUND`); no continuation. |
+| `AbstractShenyuWasmPlugin` | `doExecute(i64)` | Yes, after selector/rule
matching | Same HTTP 500/result code 533; no continuation. |
+| Either request plugin | `before(i64)`, `after(i64)` | No | No-op. |
+| `AbstractWasmPluginDataHandler` | `handlerPlugin(i64)`, `removePlugin(i64)`
| No | No-op. |
+| Same data handler | `handlerSelector(i64)`, `removeSelector(i64)` | No |
No-op. |
+| Same data handler | `handlerRule(i64)`, `removeRule(i64)` | No | No-op. |
+| `AbstractWasmMetaDataHandler` | `handleMetaData(i64)`, `removeMetaData(i64)`
| Yes, for the corresponding operation | `ShenyuWasmInitException`. |
+| Same metadata handler | `refresh()` | No | No-op. |
+| `AbstractWasmDiscoveryHandler` | `handlerDiscoveryUpstreamData(i64)` | Yes |
`ShenyuWasmInitException`. |
+| Any loader | `_initialize()` | No | No-op during loading. |
+
+An export with a matching name but the wrong kind (for example a global instead
+of a function) is invalid, not an absent optional hook.
+
+Request invocation arguments include the exchange and chain. The matched base
+plugin also supplies `SelectorData` and `RuleData`. `before`/`after` receive an
+exchange with null chain/selector/rule. Plugin data hooks receive their DTO;
+metadata/discovery hooks receive their corresponding DTO. `refresh` has no
+invocation ID and does not populate an argument map. In particular, the legacy
+metadata fixture's use of ID zero during refresh is its own adapter convention.
+
+Java publishes an argument immediately before entering the guest and removes it
+in `finally`, including on traps or callback failures. It has already been
+removed when the Java `doExecute(..., argumentId)` continuation runs. Copy the
+result into request-owned storage during `put_result`; retaining just the ID
+cannot retrieve the argument later. IDs must be unique across concurrent
+invocations sharing the static argument maps, including plugin instances.
+The default plugin-data ID methods return zero and must be overridden for
+concurrent use.
+
+The gateway's `before`/`after` calls surround construction of the plugin's
+reactive publisher, not completion of the asynchronous HTTP response. Guests
+must not use `after` as an asynchronous response-completion hook.
+
+A concurrent argument map does not make a Chicory instance or its linear memory
+safe for overlapping calls. Serialize access to a shared instance, including
+its callbacks and result copying, or use isolated instances. The reference
+plugin serializes synchronous guest execution and stores its copied decision on
+the exchange; its data handler serializes all six configuration operations.
+The abstract libraries do not provide scheduling, pooling, execution deadlines
+or automatic retries.
+
+## Reference request profile v1
+
+The [gateway reference
test](../shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/WasmGatewayTest.java)
+bootstraps a Spring Boot WebFlux server with the same
+`shenyu-spring-boot-starter-gateway` used by `shenyu-bootstrap`. Its concrete
+[Java
plugin](../shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPlugin.java)
+and [configuration
handler](../shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPluginDataHandler.java)
+are Spring beans discovered by the real gateway configuration. HTTP requests
+travel through `ShenyuWebHandler`; configuration enters through the real
+`PluginDataSubscriber`, matching cache and guest configuration hooks.
+
+The [WAT
guest](../shenyu-plugin-wasm-base/src/test/wasm-gateway/reference-plugin.wat)
+allows a request when `X-Token` equals the rule handle **and** the selector
handle
+is a prefix of the request path. The guest decides; Java translates the copied
+decision to either `chain.execute(exchange)` or HTTP 403. A terminal Java
plugin
+adds `X-Chain-Reached: true` and responds with the guest's echoed fields.
Denied
+requests have no chain header. Unmatched/disabled/deleted configurations bypass
+the WASM execution and continue normally. This example is a test reference,
+not a production authentication plugin.
+
+### Request frame
+
+All integers are four-byte little-endian values. The frame starts with version
+`1`, followed by ten fields. Each field is `byteLength: i32` followed by that
+many UTF-8 bytes, with no terminator or padding:
+
+1. selector ID
+2. selector handle (path prefix)
+3. rule ID
+4. rule handle (accepted token)
+5. HTTP method
+6. raw request path
+7. raw query, or empty string
+8. `X-Token`, or empty string
+9. `X-Request-Id`, or empty string
+10. UTF-8 request body, or empty string
+
+The guest owns one 64-KiB memory page. It reads the frame at offset 16 and
leaves
+four bytes at offset 12 for its decision. The adapter buffers at most 32 KiB of
+HTTP body for this example; a production implementation must specify its own
+body limits, replay and streaming behavior. The terminal example consumes the
+copied frame and does not forward the body to an upstream service.
+
+| Import outcome | Reference code | Behavior |
+| --- | --- | --- |
+| `get_args` success | Positive payload byte count | Full frame written. |
+| No argument for this ID | `-2` | No memory write. |
+| Buffer too small | `-1` | No partial write; guest retries with its full
65520-byte buffer. |
+| `put_result` success | `0` | Copy and decode result onto the exchange. |
+| `put_result` unknown ID | `-2` | No result accepted. |
+| Invalid memory, version, decision or frame | Exception/trap | Request fails;
no chain continuation; argument removed. |
+
+The guest deliberately starts with capacity one and requires `-1` before
+retrying. A frame still too large for the full buffer fails the guest
invocation;
+there is no dynamic memory growth or truncation. The result is a decision i32
+(`0` allow, `1` deny) followed by the original request frame. Version and field
+lengths are validated by the Java decoder; malformed results fail the request.
+
+### Configuration frame
+
+The handler's `get_args` returns the complete UTF-8 Gson serialization of the
+`PluginData`, `SelectorData` or `RuleData` DTO, with the same `-1`/`-2` policy.
+The configuration guest acknowledges the unchanged JSON through `put_result`,
+prefixed with an i32 event: `0` plugin update, `1` selector update, `2` rule
update,
+`3` plugin delete, `4` selector delete, `5` rule delete. Tests compare the
actual
+guest acknowledgement with the sent DTO and then verify subsequent HTTP
behavior.
+The handler and request guest are separate instances; the Java matching cache
+delivers the latest matched handles to the request guest. This profile does not
+promise stable DTO JSON across future ShenYu versions.
+
+## Compatibility and tested toolchains
+
+Host ABI v1 is documented, not negotiated on the wire: there is no automatic
+`abi_version` import/export or loader rejection based on this document's
version.
+An adapter must select compatible guests. Changing import types/names, pointer
+widths, required exports or payload/error semantics requires a new version of
+the affected contract; optional exports may be added without making older
guests
+implement them. A plugin-specific payload change needs a profile version
change,
+not necessarily a new host ABI. The reference frame's version is checked by
both
+sides, so it must stay at 1 unless both adapter and guest are updated.
+
+| Fixture family | Target/build | What tests establish |
+| --- | --- | --- |
+| Existing Rust fixtures | Rust edition 2021 `cdylib`, `cargo build --target
wasm32-wasip1 --release` | WASI preview1, imports, lifecycle and string
communication for the checked-in binaries. |
+| Existing Go fixtures | **TinyGo**, `tinygo build -target wasm-unknown -opt=2
-no-debug -panic=trap` | TinyGo library exports and `_initialize` for the
checked-in binaries. |
+| Gateway reference and failure fixtures | WABT `wat2wasm` 1.0.42, wasm32 |
Exact ABI types, no optional hooks, short-buffer handling,
HTTP/configuration/chain integration, concurrency, traps and bad memory. |
+
+The older Rust/TinyGo binaries do not record exact compiler versions. Their
+source build commands are documented below, but byte-for-byte reproduction of
+those legacy binaries is not claimed. The Go `go.mod` language version alone is
+not evidence of support for arbitrary Go runtimes. In particular, standard Go
+`GOOS=wasip1 GOARCH=wasm`, JavaScript `GOOS=js`, Go schedulers and all WASI
features
+are not covered by these tests. The helper under
+`shenyu-plugin-wasm-base/src/test/pkg/wasmabi` is for internal tests, not a
+published SDK; it clamps large `get_args` counts and discards `put_result`
codes,
+so it must not be assumed to implement the reference profile's error handling.
+
+## Reproduce the fixtures and tests
+
+The [WASM base README](../shenyu-plugin-wasm-base/README.md) lists the gateway
+command and fixture builds. Tests load checked-in `.wasm` files and need Java
17
+only; Rust, TinyGo and WABT are only needed to rebuild fixtures. No Docker,
+ShenYu Admin, database or external upstream is required. Configuration is sent
+directly to the gateway-side subscriber, so admin persistence and network data
+sync transports are outside the test's scope.
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
b/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
index e3f1f7140b..14fcf64b07 100644
---
a/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
+++
b/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
@@ -25,7 +25,6 @@ import com.dylibso.chicory.runtime.Memory;
import com.dylibso.chicory.runtime.Store;
import com.dylibso.chicory.wasm.Parser;
import com.dylibso.chicory.wasm.WasmModule;
-import com.dylibso.chicory.wasm.types.ExportSection;
import com.dylibso.chicory.wasm.types.FunctionType;
import com.dylibso.chicory.wasm.types.ValType;
import org.apache.shenyu.plugin.wasm.api.exception.ShenyuWasmException;
@@ -137,15 +136,10 @@ public class WasmLoader implements AutoCloseable {
}
// Call _initialize if present (required by TinyGo -target
wasm-unknown).
- try {
- ExportFunction initFn = instance.export("_initialize");
- if (Objects.nonNull(initFn)) {
- initFn.apply();
- LOG.debug("Called _initialize for {}", wasmName);
- }
- } catch (com.dylibso.chicory.wasm.InvalidException e) {
- LOG.debug("No _initialize export in {}", wasmName);
- }
+ findWasmExtern("_initialize").ifPresent(initFn -> {
+ initFn.apply();
+ LOG.debug("Called _initialize for {}", wasmName);
+ });
if (Objects.isNull(instance.memory())) {
throw new ShenyuWasmInitException("memory not available in
wasm file: " + wasmName);
@@ -276,9 +270,15 @@ public class WasmLoader implements AutoCloseable {
* @return an Optional containing the ExportFunction if found, otherwise
empty
*/
public Optional<ExportFunction> getWasmExtern(final String funcName) {
- ExportSection exports = instance.module().exportSection();
- for (int index = 0; index < exports.exportCount(); index++) {
- if (funcName.equals(exports.getExport(index).name())) {
+ return findWasmExtern(funcName);
+ }
+
+ private Optional<ExportFunction> findWasmExtern(final String funcName) {
+ // Chicory throws InvalidException for an absent export. Check the
names first
+ // so optional hooks remain optional without hiding invalid export
types.
+ var exports = instance.module().exportSection();
+ for (int i = 0; i < exports.exportCount(); i++) {
+ if (exports.getExport(i).name().equals(funcName)) {
return Optional.of(instance.export(funcName));
}
}
diff --git a/shenyu-plugin/shenyu-plugin-wasm-api/src/test/README.md
b/shenyu-plugin/shenyu-plugin-wasm-api/src/test/README.md
index 645154bb2d..0118d480c8 100644
--- a/shenyu-plugin/shenyu-plugin-wasm-api/src/test/README.md
+++ b/shenyu-plugin/shenyu-plugin-wasm-api/src/test/README.md
@@ -1,4 +1,10 @@
-# How to build the wasm file
+# WASM API fixtures
+
+See the versioned [host ABI v1](../../ABI-v1.md) and the
+[real gateway reference test](../../../shenyu-plugin-wasm-base/README.md).
+These are fixture builds, not a statement of support for arbitrary Go runtimes.
+
+## Build the Rust fixture
1. install rustup
@@ -17,3 +23,25 @@ in
`{shenyu}/shenyu-plugin/shenyu-plugin-wasm-api/src/test/rust-wasm-plugin/targ
4. rename the wasm file
rename the file to
`org.apache.shenyu.plugin.wasm.api.AbstractWasmPluginTest$RustWasmPlugin.wasm`
+
+
+From the repository root, the complete Rust build and copy command is:
+
+```sh
+rustup target add wasm32-wasip1
+api=shenyu-plugin/shenyu-plugin-wasm-api/src/test
+cargo build --manifest-path "$api/rust-wasm-plugin/Cargo.toml" --target
wasm32-wasip1 --release
+cp "$api/rust-wasm-plugin/target/wasm32-wasip1/release/rust_wasm_plugin.wasm" \
+
"$api/resources/org.apache.shenyu.plugin.wasm.api.AbstractWasmPluginTest\$RustWasmPlugin.wasm"
+```
+
+## Build the TinyGo fixture
+
+```sh
+make -C shenyu-plugin/shenyu-plugin-wasm-api/src/test/go-wasm-plugin build
+```
+
+This uses TinyGo `-target wasm-unknown -opt=2 -no-debug -panic=trap`. The exact
+Rust/TinyGo compiler versions used for the older checked-in binaries were not
+recorded. Run both module suites using the command in the WASM base README
after
+rebuilding.
diff --git a/shenyu-plugin/shenyu-plugin-wasm-base/README.md
b/shenyu-plugin/shenyu-plugin-wasm-base/README.md
index 84c35ee41f..7ed43b47b5 100644
--- a/shenyu-plugin/shenyu-plugin-wasm-base/README.md
+++ b/shenyu-plugin/shenyu-plugin-wasm-base/README.md
@@ -1,3 +1,95 @@
+# WASM base and gateway reference tests
+
+Read the versioned [host ABI v1 and reference profile
v1](../shenyu-plugin-wasm-api/ABI-v1.md)
+before implementing a guest or Java adapter.
+
+## Run the gateway test
+
+From the repository root, with Java 17:
+
+```sh
+./mvnw -B -ntp -pl shenyu-plugin/shenyu-plugin-wasm-base -am test \
+ -Dtest=WasmGatewayTest -Dsurefire.failIfNoSpecifiedTests=false \
+ -DskipRemoteResources=true -Djacoco.skip=true
+```
+
+This starts an embedded gateway on a random TCP port using the gateway starter,
+registers the reference plugin and data handler as Spring beans, and sends real
+HTTP requests. It verifies selector/rule handles, method, path, query, headers,
+UTF-8 body, a guest allow/deny decision, chain continuation/short circuit,
+configuration updates/deletes, unmatched requests, 60 concurrent requests,
+missing exports, guest traps and invalid memory. Every invocation must release
+its Java argument, including failure paths. Failure tests replace the guest
with
+real compiled Wasm fixtures and then verify healthy requests still succeed.
+
+To run both existing Rust/TinyGo unit suites and the gateway test:
+
+```sh
+./mvnw -B -ntp -pl
shenyu-plugin/shenyu-plugin-wasm-api,shenyu-plugin/shenyu-plugin-wasm-base \
+ -am test -Dtest='*Wasm*Test' -Dsurefire.failIfNoSpecifiedTests=false \
+ -DskipRemoteResources=true -Djacoco.skip=true
+```
+
+The gateway tests are normal Surefire tests and therefore run in module CI.
+Dependencies on Spring Boot and the gateway starter are test-scoped.
+
+## Rebuild the gateway Wasm fixtures
+
+Install WABT 1.0.42 (`wat2wasm --version`). From the repository root:
+
+```sh
+make -C shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway build
+```
+
+The Makefile maps each WAT source to the fully qualified Java resource name,
+including quoted `$` characters for nested test classes. The binaries are
+checked in so Java builds do not need a Wasm compiler. Rebuilding with the
stated
+WABT version should leave `git diff` empty for these binaries.
+
+## Rebuild the existing Rust fixtures
+
+Install Rust and the `wasm32-wasip1` target:
+
+```sh
+rustup target add wasm32-wasip1
+```
+
+From the repository root, build and copy the base fixtures:
+
+```sh
+base=shenyu-plugin/shenyu-plugin-wasm-base/src/test
+cargo build --manifest-path "$base/rust-shenyu-wasm-plugin/Cargo.toml"
--target wasm32-wasip1 --release
+cp
"$base/rust-shenyu-wasm-plugin/target/wasm32-wasip1/release/rust_shenyu_wasm_plugin.wasm"
\
+
"$base/resources/org.apache.shenyu.plugin.wasm.base.AbstractShenyuWasmPluginTest\$TestShenyuWasmPlugin.wasm"
+cargo build --manifest-path "$base/rust-plugin-data-handler/Cargo.toml"
--target wasm32-wasip1 --release
+cp
"$base/rust-plugin-data-handler/target/wasm32-wasip1/release/rust_plugin_data_handler.wasm"
\
+
"$base/resources/org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmPluginDataHandlerTest\$TestWasmPluginDataHandler.wasm"
+cargo build --manifest-path "$base/rust-meta-data-handler/Cargo.toml" --target
wasm32-wasip1 --release
+cp
"$base/rust-meta-data-handler/target/wasm32-wasip1/release/rust_meta_data_handler.wasm"
\
+
"$base/resources/org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmMetaDataHandlerTest\$TestWasmMetaDataHandler.wasm"
+cargo build --manifest-path "$base/rust-discovery-handler/Cargo.toml" --target
wasm32-wasip1 --release
+cp
"$base/rust-discovery-handler/target/wasm32-wasip1/release/rust_discovery_handler.wasm"
\
+
"$base/resources/org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmDiscoveryHandlerTest\$TestWasmPluginDiscoveryHandler.wasm"
+```
+
+The exact compiler versions of the legacy binaries were not recorded. Rebuilds
+may differ byte-for-byte; run the Java tests to verify the rebuilt artifacts.
+
+## Rebuild the existing TinyGo fixtures
+
+With TinyGo installed, from the repository root:
+
+```sh
+make -C shenyu-plugin/shenyu-plugin-wasm-base/src/test/go-shenyu-wasm-plugin
build
+make -C shenyu-plugin/shenyu-plugin-wasm-base/src/test/go-plugin-data-handler
build
+make -C shenyu-plugin/shenyu-plugin-wasm-base/src/test/go-meta-data-handler
build
+make -C shenyu-plugin/shenyu-plugin-wasm-base/src/test/go-discovery-handler
build
+```
+
+These Makefiles build with `-target wasm-unknown -opt=2 -no-debug -panic=trap`
+and copy the result to its Java resource name. The test helper and examples are
+TinyGo fixtures; they do not certify arbitrary standard Go runtimes.
+
# WASM discovery callbacks
`AbstractWasmDiscoveryHandler` forwards discovery updates to the required guest
@@ -33,3 +125,23 @@ The Go and Rust test guests read the deletion payload and
echo it through
and `src/test/rust-discovery-handler`. The
`TestLegacyWasmPluginDiscoveryHandler`
fixture preserves the original Go binary without the deletion export to test
compatibility with existing guests.
+
+## CI fixture verification
+
+The `wasm-fixtures` workflow pins WABT 1.0.42 and Rust 1.90.0. It runs
+`python3 .github/scripts/verify-wasm-fixtures.py` when either WASM module
changes.
+The script checks the gateway WAT binaries byte-for-byte without modifying the
+committed files, then compiles the five Rust guests for `wasm32-wasip1` and
runs
+the WASM Java suites against them. Original resource bytes are restored even if
+a build or test fails. Legacy Rust compiler versions were not recorded, so
+Rust reproduction is checked by behavior rather than exact binary identity.
+The existing TinyGo guests run in the same suites. Use `--wat-only` or
+`--rust-only` for a scoped local run. Maven-only builds still use committed
guests.
+
+The optional `removeDiscoveryUpstreamData` callback documented below is now on
+the merged base through #7568. The binary request profile in these gateway
tests
+belongs to the explicit Java reference adapter; it is not the JSON default
+bridge introduced separately by #7569. Both use the same host import signatures
+and the merged policy that rejects non-function exports. This documentation PR
+keeps `_initialize` validation in the same private lookup helper so a malformed
+initializer is not silently treated as an absent optional hook.
diff --git a/shenyu-plugin/shenyu-plugin-wasm-base/pom.xml
b/shenyu-plugin/shenyu-plugin-wasm-base/pom.xml
index b3e20c0b01..a19dd8a441 100644
--- a/shenyu-plugin/shenyu-plugin-wasm-base/pom.xml
+++ b/shenyu-plugin/shenyu-plugin-wasm-base/pom.xml
@@ -36,6 +36,22 @@
<artifactId>shenyu-plugin-base</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-spring-boot-starter-gateway</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-webflux</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
@@ -47,4 +63,18 @@
<scope>test</scope>
</dependency>
</dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.rat</groupId>
+ <artifactId>apache-rat-plugin</artifactId>
+ <configuration>
+ <excludes combine.children="append">
+ <!-- Compiled fixtures; the corresponding WAT sources
carry license headers. -->
+
<exclude>src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.*.wasm</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPlugin.java
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPlugin.java
new file mode 100644
index 0000000000..e200d87472
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPlugin.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.wasm.base.gateway;
+
+import com.dylibso.chicory.runtime.HostFunction;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.runtime.Store;
+import com.dylibso.chicory.wasm.types.FunctionType;
+import com.dylibso.chicory.wasm.types.ValType;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.plugin.api.ShenyuPluginChain;
+import org.apache.shenyu.plugin.api.utils.WebFluxResultUtils;
+import org.apache.shenyu.plugin.wasm.api.exception.ShenyuWasmException;
+import org.apache.shenyu.plugin.wasm.base.AbstractShenyuWasmPlugin;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Reference token gate using the binary request profile documented in
ABI-v1.md.
+ */
+public class ReferenceWasmPlugin extends AbstractShenyuWasmPlugin {
+
+ static final String NAME = "wasmReference";
+
+ static final String RESULT = ReferenceWasmPlugin.class.getName() +
".result";
+
+ private static final String BODY = ReferenceWasmPlugin.class.getName() +
".body";
+
+ private static final List<String> FIELDS = List.of("selectorId",
"selectorHandle", "ruleId", "ruleHandle",
+ "method", "path", "query", "token", "requestId", "body");
+
+ private static final AtomicLong IDS = new AtomicLong();
+
+ public ReferenceWasmPlugin() {
+ super(ReferenceWasmPlugin::registerImports);
+ }
+
+ private static void registerImports(final Store store) {
+ FunctionType type = FunctionType.of(List.of(ValType.I64, ValType.I64,
ValType.I32), List.of(ValType.I32));
+ store.addFunction(new HostFunction("shenyu", "get_args", type,
ReferenceWasmPlugin::getArgs));
+ store.addFunction(new HostFunction("shenyu", "put_result", type,
ReferenceWasmPlugin::putResult));
+ }
+
+ private static long[] getArgs(final Instance instance, final long... args)
{
+ Argument argument = ARGUMENTS.get(args[0]);
+ if (Objects.isNull(argument)) {
+ return new long[]{-2};
+ }
+ ServerWebExchange exchange = argument.getExchange();
+ List<String> fields = List.of(argument.getSelector().getId(),
argument.getSelector().getHandle(),
+ argument.getRule().getId(), argument.getRule().getHandle(),
exchange.getRequest().getMethod().name(),
+ exchange.getRequest().getURI().getRawPath(),
Objects.toString(exchange.getRequest().getURI().getRawQuery(), ""),
+
Objects.toString(exchange.getRequest().getHeaders().getFirst("X-Token"), ""),
+
Objects.toString(exchange.getRequest().getHeaders().getFirst("X-Request-Id"),
""), exchange.getAttributeOrDefault(BODY, ""));
+ List<byte[]> bytes = fields.stream().map(field ->
field.getBytes(StandardCharsets.UTF_8)).toList();
+ int size = Integer.BYTES + bytes.stream().mapToInt(field ->
Integer.BYTES + field.length).sum();
+ if (args[2] < size) {
+ // No partial write: the guest retries with its full buffer.
+ return new long[]{-1};
+ }
+ ByteBuffer frame =
ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ frame.putInt(1);
+ bytes.forEach(field -> frame.putInt(field.length).put(field));
+ instance.memory().write(Math.toIntExact(args[1]), frame.array());
+ return new long[]{size};
+ }
+
+ private static long[] putResult(final Instance instance, final long...
args) {
+ Argument argument = ARGUMENTS.get(args[0]);
+ if (Objects.isNull(argument)) {
+ return new long[]{-2};
+ }
+ byte[] bytes = instance.memory().readBytes(Math.toIntExact(args[1]),
Math.toIntExact(args[2]));
+ ByteBuffer frame =
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ int decision = frame.getInt();
+ if (frame.getInt() != 1 || decision < 0 || decision > 1) {
+ throw new ShenyuWasmException("Invalid reference result version or
decision");
+ }
+ Map<String, String> result = new LinkedHashMap<>();
+ result.put("decision", decision == 0 ? "allow" : "deny");
+ for (String field : FIELDS) {
+ int length = frame.getInt();
+ if (length < 0 || length > frame.remaining()) {
+ throw new ShenyuWasmException("Invalid reference result field
length");
+ }
+ byte[] value = new byte[length];
+ frame.get(value);
+ result.put(field, new String(value, StandardCharsets.UTF_8));
+ }
+ if (frame.hasRemaining()) {
+ throw new ShenyuWasmException("Unexpected reference result bytes");
+ }
+ argument.getExchange().getAttributes().put(RESULT, result);
+ return new long[]{0};
+ }
+
+ @Override
+ public Mono<Void> execute(final ServerWebExchange exchange, final
ShenyuPluginChain chain) {
+ // Buffer only in this example. A production adapter must define body
size and streaming policy.
+ return DataBufferUtils.join(exchange.getRequest().getBody(),
32768).map(buffer -> {
+ try {
+ byte[] body = new byte[buffer.readableByteCount()];
+ buffer.read(body);
+ return new String(body, StandardCharsets.UTF_8);
+ } finally {
+ DataBufferUtils.release(buffer);
+ }
+ }).defaultIfEmpty("").flatMap(body -> {
+ exchange.getAttributes().put(BODY, body);
+ // The loader owns one mutable instance. Hold the monitor for the
synchronous
+ // guest invocation and result decoding, then run the reactive
chain outside it.
+ synchronized (this) {
+ return super.execute(exchange, chain);
+ }
+ });
+ }
+
+ @Override
+ protected Mono<Void> doExecute(final ServerWebExchange exchange, final
ShenyuPluginChain chain,
+ final SelectorData selector, final RuleData
rule, final Long argumentId) {
+ Map<String, String> result = exchange.getAttribute(RESULT);
+ if (Objects.isNull(result)) {
+ return Mono.error(new ShenyuWasmException("Guest did not return a
decision"));
+ }
+ exchange.getResponse().getHeaders().set("X-Wasm-Decision",
result.get("decision"));
+ if ("deny".equals(result.get("decision"))) {
+ exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
+ return WebFluxResultUtils.result(exchange, result);
+ }
+ return chain.execute(exchange);
+ }
+
+ @Override
+ protected Long getArgumentId(final ServerWebExchange exchange, final
ShenyuPluginChain chain,
+ final SelectorData selector, final RuleData
rule) {
+ return IDS.incrementAndGet();
+ }
+
+ @Override
+ public int getOrder() {
+ return -100;
+ }
+
+ @Override
+ public String named() {
+ return NAME;
+ }
+
+ static int pendingArguments() {
+ return ARGUMENTS.size();
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPluginDataHandler.java
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPluginDataHandler.java
new file mode 100644
index 0000000000..57a36c2197
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/ReferenceWasmPluginDataHandler.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.wasm.base.gateway;
+
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.utils.GsonUtils;
+import
org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmPluginDataHandler;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Configuration hooks acknowledge the exact JSON received by the guest.
+ */
+public final class ReferenceWasmPluginDataHandler extends
AbstractWasmPluginDataHandler {
+
+ private static final AtomicLong IDS = new AtomicLong();
+
+ private final List<Map<String, Object>> events = new
CopyOnWriteArrayList<>();
+
+ @Override
+ protected long onGetArgs(final long argId, final long addr, final int len)
{
+ Object data = PLUGIN_ARGUMENTS.get(argId);
+ if (Objects.isNull(data)) {
+ data = SELECTOR_ARGUMENTS.get(argId);
+ }
+ if (Objects.isNull(data)) {
+ data = RULE_ARGUMENTS.get(argId);
+ }
+ if (Objects.isNull(data)) {
+ return -2;
+ }
+ byte[] bytes =
GsonUtils.getInstance().toJson(data).getBytes(StandardCharsets.UTF_8);
+ if (bytes.length > len) {
+ return -1;
+ }
+ getMemory().write(Math.toIntExact(addr), bytes);
+ return bytes.length;
+ }
+
+ @Override
+ protected long onPutResult(final long argId, final long addr, final int
len) {
+ byte[] bytes = getMemory().readBytes(Math.toIntExact(addr), len);
+ int event =
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ String json = new String(bytes, Integer.BYTES, bytes.length -
Integer.BYTES, StandardCharsets.UTF_8);
+ events.add(Map.of("event", event, "json", json));
+ return 0;
+ }
+
+ @Override
+ protected Long getPluginArgumentId(final PluginData data) {
+ return IDS.incrementAndGet();
+ }
+
+ @Override
+ protected Long getSelectorArgumentId(final SelectorData data) {
+ return IDS.incrementAndGet();
+ }
+
+ @Override
+ protected Long getRuleArgumentId(final RuleData data) {
+ return IDS.incrementAndGet();
+ }
+
+ @Override
+ public synchronized void handlerPlugin(final PluginData data) {
+ super.handlerPlugin(data);
+ }
+
+ @Override
+ public synchronized void removePlugin(final PluginData data) {
+ super.removePlugin(data);
+ }
+
+ @Override
+ public synchronized void handlerSelector(final SelectorData data) {
+ super.handlerSelector(data);
+ }
+
+ @Override
+ public synchronized void removeSelector(final SelectorData data) {
+ super.removeSelector(data);
+ }
+
+ @Override
+ public synchronized void handlerRule(final RuleData data) {
+ super.handlerRule(data);
+ }
+
+ @Override
+ public synchronized void removeRule(final RuleData data) {
+ super.removeRule(data);
+ }
+
+ @Override
+ public String pluginNamed() {
+ return ReferenceWasmPlugin.NAME;
+ }
+
+ List<Map<String, Object>> events() {
+ return events;
+ }
+
+ static int pendingArguments() {
+ return PLUGIN_ARGUMENTS.size() + SELECTOR_ARGUMENTS.size() +
RULE_ARGUMENTS.size();
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/WasmGatewayTest.java
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/WasmGatewayTest.java
new file mode 100644
index 0000000000..f04fe1a03e
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/WasmGatewayTest.java
@@ -0,0 +1,364 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.wasm.base.gateway;
+
+import org.apache.shenyu.common.dto.ConditionData;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.SelectorTypeEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.plugin.api.ShenyuPlugin;
+import org.apache.shenyu.plugin.api.ShenyuPluginChain;
+import org.apache.shenyu.plugin.api.result.ShenyuResultEnum;
+import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
+import org.apache.shenyu.plugin.api.utils.WebFluxResultUtils;
+import org.apache.shenyu.plugin.base.cache.BaseDataCache;
+import org.apache.shenyu.plugin.base.cache.MatchDataCache;
+import
org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmPluginDataHandler;
+import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
+import org.apache.shenyu.web.handler.ShenyuWebHandler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Bootstraps the gateway starter on a real HTTP port with a registered WASM
plugin.
+ */
+@SpringBootTest(classes = WasmGatewayTest.GatewayApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ properties = {"spring.main.web-application-type=reactive",
"spring.main.allow-bean-definition-overriding=true",
+ "shenyu.local.enabled=false", "shenyu.alert.enabled=false"})
+public final class WasmGatewayTest {
+
+ @Autowired
+ private ConfigurableApplicationContext context;
+
+ @Autowired
+ private PluginDataSubscriber subscriber;
+
+ @Autowired
+ private ShenyuWebHandler webHandler;
+
+ @Autowired
+ private ReferenceWasmPlugin plugin;
+
+ @Autowired
+ private ReferenceWasmPluginDataHandler handler;
+
+ @LocalServerPort
+ private int port;
+
+ private WebTestClient client;
+
+ private PluginData pluginData;
+
+ private SelectorData selector;
+
+ private RuleData rule;
+
+ @BeforeEach
+ public void setUp() {
+ // Other module tests install mock contexts in this static utility.
+ SpringBeanUtils.getInstance().setApplicationContext(context);
+ clearCaches();
+ handler.events().clear();
+ webHandler.putExtPlugins(List.of(plugin));
+ client = WebTestClient.bindToServer().baseUrl("http://localhost:" +
port).responseTimeout(Duration.ofSeconds(15)).build();
+ pluginData =
PluginData.builder().id("wasm-plugin").name(ReferenceWasmPlugin.NAME).enabled(true).sort(-100).config("v1").build();
+ selector =
SelectorData.builder().id("wasm-selector").pluginName(ReferenceWasmPlugin.NAME).enabled(true).sort(0)
+
.type(SelectorTypeEnum.CUSTOM_FLOW.getCode()).continued(true).matchMode(0).matchRestful(false).logged(false)
+
.handle("/wasm/").conditionList(List.of(uriCondition())).build();
+ rule =
RuleData.builder().id("wasm-rule").selectorId(selector.getId()).pluginName(ReferenceWasmPlugin.NAME)
+
.enabled(true).sort(0).matchMode(0).matchRestful(false).loged(false).handle("secret")
+ .conditionDataList(List.of(uriCondition())).build();
+ subscriber.onSubscribe(pluginData);
+ subscriber.onSelectorSubscribe(selector);
+ subscriber.onRuleSubscribe(rule);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ assertEquals(0, ReferenceWasmPlugin.pendingArguments());
+ assertEquals(0, ReferenceWasmPluginDataHandler.pendingArguments());
+ clearCaches();
+ webHandler.putExtPlugins(List.of(plugin));
+ }
+
+ @Test
+ public void requestDataAndGuestDecisionControlTheChain() {
+ assertTrue(webHandler.getPlugins().contains(plugin));
+ request("secret", "request-1", "hello 世界").expectStatus().isOk()
+ .expectHeader().valueEquals("X-Wasm-Decision", "allow")
+ .expectHeader().valueEquals("X-Chain-Reached", "true")
+
.expectBody().jsonPath("$.selectorId").isEqualTo(selector.getId())
+ .jsonPath("$.selectorHandle").isEqualTo("/wasm/")
+
.jsonPath("$.ruleId").isEqualTo(rule.getId()).jsonPath("$.ruleHandle").isEqualTo("secret")
+
.jsonPath("$.method").isEqualTo("POST").jsonPath("$.path").isEqualTo("/wasm/check")
+
.jsonPath("$.query").isEqualTo("source=gateway").jsonPath("$.token").isEqualTo("secret")
+
.jsonPath("$.requestId").isEqualTo("request-1").jsonPath("$.body").isEqualTo("hello
世界");
+ request("wrong", "request-2", "rejected").expectStatus().isForbidden()
+ .expectHeader().valueEquals("X-Wasm-Decision",
"deny").expectHeader().doesNotExist("X-Chain-Reached")
+
.expectBody().jsonPath("$.decision").isEqualTo("deny").jsonPath("$.requestId").isEqualTo("request-2");
+ }
+
+ @Test
+ public void
configurationUpdatesAndDeletesReachTheGuestAndInvalidateMatching() {
+ assertEvent(0, 0, pluginData);
+ assertEvent(1, 1, selector);
+ assertEvent(2, 2, rule);
+ request("secret", "initial", "").expectStatus().isOk();
+ rule.setHandle("updated");
+ subscriber.onRuleSubscribe(rule);
+ assertEvent(3, 2, rule);
+ request("secret", "old-token", "").expectStatus().isForbidden();
+ request("updated", "new-token", "").expectStatus().isOk();
+ selector.setHandle("/different/");
+ subscriber.onSelectorSubscribe(selector);
+ assertEvent(4, 1, selector);
+ request("updated", "new-selector", "").expectStatus().isForbidden();
+ selector.setHandle("/wasm/");
+ subscriber.onSelectorSubscribe(selector);
+ subscriber.unRuleSubscribe(rule);
+ assertEvent(6, 5, rule);
+ expectBypass();
+ subscriber.onRuleSubscribe(rule);
+ request("updated", "rule-restored", "").expectStatus().isOk();
+ subscriber.unSelectorSubscribe(selector);
+ assertEvent(8, 4, selector);
+ expectBypass();
+ subscriber.onSelectorSubscribe(selector);
+ subscriber.onRuleSubscribe(rule);
+ request("updated", "selector-restored", "").expectStatus().isOk();
+ pluginData.setConfig("v2");
+ pluginData.setEnabled(false);
+ subscriber.onSubscribe(pluginData);
+ assertEvent(11, 0, pluginData);
+ expectBypass();
+ pluginData.setEnabled(true);
+ subscriber.onSubscribe(pluginData);
+ request("updated", "plugin-restored", "").expectStatus().isOk();
+ subscriber.unSubscribe(pluginData);
+ assertEvent(13, 3, pluginData);
+ expectBypass();
+ }
+
+ @Test
+ public void unmatchedRequestsContinueWithoutInvokingTheGuest() {
+ client.get().uri("/outside").exchange().expectStatus().isOk()
+
.expectHeader().doesNotExist("X-Wasm-Decision").expectHeader().valueEquals("X-Chain-Reached",
"true");
+ }
+
+ @Test
+ public void concurrentRequestsKeepArgumentsMemoryAndResultsIsolated()
throws Exception {
+ ExecutorService executor = Executors.newFixedThreadPool(12);
+ try {
+ List<Callable<Void>> calls = new ArrayList<>();
+ for (int i = 0; i < 60; i++) {
+ final String id = "parallel-" + i;
+ final boolean allow = i % 2 == 0;
+ calls.add(() -> {
+ WebTestClient.ResponseSpec response = request(allow ?
"secret" : id, id, "body-" + id);
+ response.expectStatus().isEqualTo(allow ? HttpStatus.OK :
HttpStatus.FORBIDDEN);
+ if (allow) {
+ response.expectHeader().valueEquals("X-Chain-Reached",
"true");
+ } else {
+
response.expectHeader().doesNotExist("X-Chain-Reached");
+ }
+
response.expectBody().jsonPath("$.decision").isEqualTo(allow ? "allow" : "deny")
+
.jsonPath("$.requestId").isEqualTo(id).jsonPath("$.body").isEqualTo("body-" +
id);
+ return null;
+ });
+ }
+ for (Future<Void> future : executor.invokeAll(calls, 30,
TimeUnit.SECONDS)) {
+ future.get();
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void missingRequiredExecutionReturnsTheGatewayError() {
+ webHandler.putExtPlugins(List.of(new MissingExecutePlugin()));
+ request("secret", "missing-export",
"").expectStatus().is5xxServerError()
+ .expectHeader().doesNotExist("X-Chain-Reached")
+
.expectBody().jsonPath("$.code").isEqualTo(ShenyuResultEnum.WASM_FUNC_NOT_FOUND.getCode());
+ }
+
+ @Test
+ public void missingConfigurationHooksAreOptional() {
+ try (MissingHooksHandler missing = new MissingHooksHandler()) {
+ missing.handlerPlugin(pluginData);
+ missing.handlerSelector(selector);
+ missing.handlerRule(rule);
+ missing.removeRule(rule);
+ missing.removeSelector(selector);
+ missing.removePlugin(pluginData);
+ assertFalse(missing.getWasmExtern("handlerRule").isPresent());
+ }
+ }
+
+ @Test
+ public void guestTrapFailsTheRequestAndReleasesArguments() {
+ webHandler.putExtPlugins(List.of(new TrappingPlugin()));
+ expectFailureAndRecovery();
+ }
+
+ @Test
+ public void invalidGuestMemoryFailsTheRequestAndReleasesArguments() {
+ webHandler.putExtPlugins(List.of(new InvalidMemoryPlugin()));
+ expectFailureAndRecovery();
+ }
+
+ @Test
+ public void unsupportedGuestResultVersionFailsTheRequest() {
+ webHandler.putExtPlugins(List.of(new IncompatibleResultPlugin()));
+ expectFailureAndRecovery();
+ }
+
+ private void expectFailureAndRecovery() {
+ request("secret", "failure",
"").expectStatus().is5xxServerError().expectHeader().doesNotExist("X-Chain-Reached");
+ assertEquals(0, ReferenceWasmPlugin.pendingArguments());
+ webHandler.putExtPlugins(List.of(plugin));
+ request("secret", "recovery", "after failure").expectStatus().isOk()
+ .expectBody().jsonPath("$.requestId").isEqualTo("recovery");
+ }
+
+ private WebTestClient.ResponseSpec request(final String token, final
String id, final String body) {
+ return
client.post().uri("/wasm/check?source=gateway").header("X-Token",
token).header("X-Request-Id", id)
+ .contentType(MediaType.TEXT_PLAIN).bodyValue(body).exchange();
+ }
+
+ private void expectBypass() {
+ request("updated", "bypass",
"").expectStatus().isOk().expectHeader().doesNotExist("X-Wasm-Decision")
+ .expectHeader().valueEquals("X-Chain-Reached", "true");
+ }
+
+ private void assertEvent(final int index, final int event, final Object
data) {
+ assertEquals(event, handler.events().get(index).get("event"));
+ assertEquals(GsonUtils.getInstance().toJson(data),
handler.events().get(index).get("json"));
+ }
+
+ private static ConditionData uriCondition() {
+ ConditionData condition = new ConditionData();
+ condition.setParamType("uri");
+ condition.setParamName("/");
+ condition.setOperator("match");
+ condition.setParamValue("/wasm/**");
+ return condition;
+ }
+
+ private static void clearCaches() {
+ BaseDataCache.getInstance().cleanPluginData();
+ BaseDataCache.getInstance().cleanSelectorData();
+ BaseDataCache.getInstance().cleanRuleData();
+ MatchDataCache.getInstance().cleanSelectorData();
+ MatchDataCache.getInstance().cleanRuleDataData();
+ }
+
+ /**
+ * Minimal gateway bootstrap: the same gateway starter used by
shenyu-bootstrap.
+ */
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class GatewayApplication {
+
+ @Bean
+ ReferenceWasmPlugin referenceWasmPlugin() {
+ return new ReferenceWasmPlugin();
+ }
+
+ @Bean
+ ReferenceWasmPluginDataHandler referenceWasmPluginDataHandler() {
+ return new ReferenceWasmPluginDataHandler();
+ }
+
+ @Bean
+ ShenyuPlugin terminalPlugin() {
+ return new ShenyuPlugin() {
+ @Override
+ public Mono<Void> execute(final ServerWebExchange exchange,
final ShenyuPluginChain chain) {
+ exchange.getResponse().getHeaders().set("X-Chain-Reached",
"true");
+ Object result =
exchange.getAttribute(ReferenceWasmPlugin.RESULT);
+ return WebFluxResultUtils.result(exchange,
Objects.nonNull(result) ? result : Map.of("bypass", true));
+ }
+
+ @Override
+ public int getOrder() {
+ return -90;
+ }
+
+ @Override
+ public String named() {
+ return "wasmTerminal";
+ }
+ };
+ }
+ }
+
+ /** Loads the memory-only fixture without a doExecute export. */
+ static final class MissingExecutePlugin extends ReferenceWasmPlugin {
+ }
+
+ /** Loads a guest that traps after reading its arguments. */
+ static final class TrappingPlugin extends ReferenceWasmPlugin {
+ }
+
+ /** Loads a guest that passes an out-of-bounds result pointer. */
+ static final class InvalidMemoryPlugin extends ReferenceWasmPlugin {
+ }
+
+ /** Loads a guest returning an unsupported reference profile version. */
+ static final class IncompatibleResultPlugin extends ReferenceWasmPlugin {
+ }
+
+ /** Loads a handler with none of the optional configuration exports. */
+ static final class MissingHooksHandler extends
AbstractWasmPluginDataHandler {
+ @Override
+ public String pluginNamed() {
+ return ReferenceWasmPlugin.NAME;
+ }
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/WasmLifecycleContractTest.java
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/WasmLifecycleContractTest.java
new file mode 100644
index 0000000000..73fea71fa2
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/java/org/apache/shenyu/plugin/wasm/base/gateway/WasmLifecycleContractTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.shenyu.plugin.wasm.base.gateway;
+
+import com.dylibso.chicory.wasm.InvalidException;
+import org.apache.shenyu.common.dto.DiscoverySyncData;
+import org.apache.shenyu.common.dto.MetaData;
+import org.apache.shenyu.plugin.wasm.api.exception.ShenyuWasmInitException;
+import org.apache.shenyu.plugin.wasm.api.loader.WasmLoader;
+import org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmDiscoveryHandler;
+import org.apache.shenyu.plugin.wasm.base.handler.AbstractWasmMetaDataHandler;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Verifies optional and required lifecycle exports against actual Wasm
modules.
+ */
+public final class WasmLifecycleContractTest {
+
+ @Test
+ public void absentMetadataOperationsAreRequiredButRefreshIsOptional() {
+ try (MissingMetadataHandler handler = new MissingMetadataHandler()) {
+ assertThrows(ShenyuWasmInitException.class, () ->
handler.handle(new MetaData()));
+ assertThrows(ShenyuWasmInitException.class, () ->
handler.remove(new MetaData()));
+ handler.refresh();
+ assertFalse(handler.getWasmExtern("_initialize").isPresent());
+ }
+ }
+
+ @Test
+ public void absentDiscoveryOperationIsRequired() {
+ try (MissingDiscoveryHandler handler = new MissingDiscoveryHandler()) {
+ assertThrows(ShenyuWasmInitException.class, () ->
handler.handlerDiscoveryUpstreamData(new DiscoverySyncData()));
+ }
+ }
+
+ @Test
+ public void exportOfTheWrongKindIsNotAnAbsentOptionalFunction() {
+ try (InvalidHookLoader loader = new InvalidHookLoader()) {
+ assertThrows(InvalidException.class, () ->
loader.getWasmExtern("before"));
+ }
+ }
+
+ @Test
+ public void invalidInitializerIsNotIgnored() {
+ assertThrows(InvalidException.class, InvalidInitializerLoader::new);
+ }
+
+ /** Memory-only metadata fixture. */
+ static final class MissingMetadataHandler extends
AbstractWasmMetaDataHandler {
+ @Override
+ public String rpcType() {
+ return "http";
+ }
+
+ @Override
+ protected Long getArgumentId(final MetaData data) {
+ return 1L;
+ }
+ }
+
+ /** Memory-only discovery fixture. */
+ static final class MissingDiscoveryHandler extends
AbstractWasmDiscoveryHandler {
+ @Override
+ public String pluginName() {
+ return ReferenceWasmPlugin.NAME;
+ }
+
+ @Override
+ protected Long getArgumentId(final DiscoverySyncData data) {
+ return 1L;
+ }
+ }
+
+ /** Fixture exporting a global named before. */
+ static final class InvalidHookLoader extends WasmLoader {
+ }
+
+ /** Fixture exporting a global named _initialize. */
+ static final class InvalidInitializerLoader extends WasmLoader {
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.ReferenceWasmPlugin.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.ReferenceWasmPlugin.wasm
new file mode 100644
index 0000000000..104ff1d6f7
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.ReferenceWasmPlugin.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.ReferenceWasmPluginDataHandler.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.ReferenceWasmPluginDataHandler.wasm
new file mode 100644
index 0000000000..4bd0c334b5
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.ReferenceWasmPluginDataHandler.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$IncompatibleResultPlugin.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$IncompatibleResultPlugin.wasm
new file mode 100644
index 0000000000..636f5b4ce7
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$IncompatibleResultPlugin.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$InvalidMemoryPlugin.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$InvalidMemoryPlugin.wasm
new file mode 100644
index 0000000000..8f7d4f8d1b
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$InvalidMemoryPlugin.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$MissingExecutePlugin.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$MissingExecutePlugin.wasm
new file mode 100644
index 0000000000..3a12363fcf
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$MissingExecutePlugin.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$MissingHooksHandler.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$MissingHooksHandler.wasm
new file mode 100644
index 0000000000..3a12363fcf
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$MissingHooksHandler.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$TrappingPlugin.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$TrappingPlugin.wasm
new file mode 100644
index 0000000000..12dfaee0c4
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmGatewayTest$TrappingPlugin.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$InvalidHookLoader.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$InvalidHookLoader.wasm
new file mode 100644
index 0000000000..2de457dd86
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$InvalidHookLoader.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$InvalidInitializerLoader.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$InvalidInitializerLoader.wasm
new file mode 100644
index 0000000000..e3a3a7ebea
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$InvalidInitializerLoader.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$MissingDiscoveryHandler.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$MissingDiscoveryHandler.wasm
new file mode 100644
index 0000000000..3a12363fcf
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$MissingDiscoveryHandler.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$MissingMetadataHandler.wasm
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$MissingMetadataHandler.wasm
new file mode 100644
index 0000000000..3a12363fcf
Binary files /dev/null and
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/resources/org.apache.shenyu.plugin.wasm.base.gateway.WasmLifecycleContractTest$MissingMetadataHandler.wasm
differ
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/Makefile
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/Makefile
new file mode 100644
index 0000000000..7519028a5a
--- /dev/null
+++ b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/Makefile
@@ -0,0 +1,36 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+WAT2WASM ?= wat2wasm
+RESOURCES := ../resources
+PREFIX := org.apache.shenyu.plugin.wasm.base.gateway.
+
+.PHONY: build
+build:
+ $(WAT2WASM) reference-plugin.wat -o
$(RESOURCES)/$(PREFIX)ReferenceWasmPlugin.wasm
+ $(WAT2WASM) configuration-handler.wat -o
$(RESOURCES)/$(PREFIX)ReferenceWasmPluginDataHandler.wasm
+ $(WAT2WASM) missing-hooks.wat -o
'$(RESOURCES)/$(PREFIX)WasmGatewayTest$$MissingExecutePlugin.wasm'
+ $(WAT2WASM) missing-hooks.wat -o
'$(RESOURCES)/$(PREFIX)WasmGatewayTest$$MissingHooksHandler.wasm'
+ $(WAT2WASM) trap.wat -o
'$(RESOURCES)/$(PREFIX)WasmGatewayTest$$TrappingPlugin.wasm'
+ $(WAT2WASM) invalid-memory.wat -o
'$(RESOURCES)/$(PREFIX)WasmGatewayTest$$InvalidMemoryPlugin.wasm'
+ $(WAT2WASM) missing-hooks.wat -o
'$(RESOURCES)/$(PREFIX)WasmLifecycleContractTest$$MissingMetadataHandler.wasm'
+ $(WAT2WASM) missing-hooks.wat -o
'$(RESOURCES)/$(PREFIX)WasmLifecycleContractTest$$MissingDiscoveryHandler.wasm'
+ $(WAT2WASM) invalid-hook.wat -o
'$(RESOURCES)/$(PREFIX)WasmLifecycleContractTest$$InvalidHookLoader.wasm'
+ $(WAT2WASM) invalid-initializer.wat -o
'$(RESOURCES)/$(PREFIX)WasmLifecycleContractTest$$InvalidInitializerLoader.wasm'
+ $(WAT2WASM) incompatible-result.wat -o
'$(RESOURCES)/$(PREFIX)WasmGatewayTest$$IncompatibleResultPlugin.wasm'
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/configuration-handler.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/configuration-handler.wat
new file mode 100644
index 0000000000..e154032d45
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/configuration-handler.wat
@@ -0,0 +1,34 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module
+ (import "shenyu" "get_args" (func $get_args (param i64 i64 i32) (result
i32)))
+ (import "shenyu" "put_result" (func $put_result (param i64 i64 i32) (result
i32)))
+ (memory (export "memory") 1)
+ (func $ack (param $id i64) (param $event i32)
+ (local $length i32)
+ (local.set $length (call $get_args (local.get $id) (i64.const 16)
(i32.const 65520)))
+ (if (i32.le_s (local.get $length) (i32.const 0)) (then unreachable))
+ (i32.store (i32.const 12) (local.get $event))
+ (if (call $put_result (local.get $id) (i64.const 12)
+ (i32.add (local.get $length) (i32.const 4))) (then unreachable)))
+ (func (export "handlerPlugin") (param $id i64) (call $ack (local.get $id)
(i32.const 0)))
+ (func (export "handlerSelector") (param $id i64) (call $ack (local.get $id)
(i32.const 1)))
+ (func (export "handlerRule") (param $id i64) (call $ack (local.get $id)
(i32.const 2)))
+ (func (export "removePlugin") (param $id i64) (call $ack (local.get $id)
(i32.const 3)))
+ (func (export "removeSelector") (param $id i64) (call $ack (local.get $id)
(i32.const 4)))
+ (func (export "removeRule") (param $id i64) (call $ack (local.get $id)
(i32.const 5))))
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/incompatible-result.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/incompatible-result.wat
new file mode 100644
index 0000000000..8c981ebfa2
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/incompatible-result.wat
@@ -0,0 +1,24 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module
+ (import "shenyu" "put_result" (func $put_result (param i64 i64 i32) (result
i32)))
+ (memory (export "memory") 1)
+ (func (export "doExecute") (param $id i64)
+ (i32.store (i32.const 12) (i32.const 0))
+ (i32.store (i32.const 16) (i32.const 2))
+ (drop (call $put_result (local.get $id) (i64.const 12) (i32.const 8)))))
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-hook.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-hook.wat
new file mode 100644
index 0000000000..1a1e48910e
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-hook.wat
@@ -0,0 +1,20 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module
+ (memory (export "memory") 1)
+ (global (export "before") i32 (i32.const 0)))
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-initializer.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-initializer.wat
new file mode 100644
index 0000000000..0a7dc2c61b
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-initializer.wat
@@ -0,0 +1,20 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module
+ (memory (export "memory") 1)
+ (global (export "_initialize") i32 (i32.const 0)))
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-memory.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-memory.wat
new file mode 100644
index 0000000000..cf215b1566
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/invalid-memory.wat
@@ -0,0 +1,22 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module
+ (import "shenyu" "put_result" (func $put_result (param i64 i64 i32) (result
i32)))
+ (memory (export "memory") 1)
+ (func (export "doExecute") (param $id i64)
+ (drop (call $put_result (local.get $id) (i64.const 65535) (i32.const 8)))))
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/missing-hooks.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/missing-hooks.wat
new file mode 100644
index 0000000000..532ccc4fa9
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/missing-hooks.wat
@@ -0,0 +1,18 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module (memory (export "memory") 1))
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/reference-plugin.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/reference-plugin.wat
new file mode 100644
index 0000000000..3ba4fa6700
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/reference-plugin.wat
@@ -0,0 +1,72 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module
+ (import "shenyu" "get_args" (func $get_args (param i64 i64 i32) (result
i32)))
+ (import "shenyu" "put_result" (func $put_result (param i64 i64 i32) (result
i32)))
+ (memory (export "memory") 1)
+
+ ;; Return the address of the length prefix of field n in the request frame.
+ (func $field (param $n i32) (result i32)
+ (local $p i32)
+ (local.set $p (i32.const 20))
+ (block $done
+ (loop $next
+ (br_if $done (i32.eqz (local.get $n)))
+ (local.set $p (i32.add (local.get $p)
+ (i32.add (i32.const 4) (i32.load (local.get $p)))))
+ (local.set $n (i32.sub (local.get $n) (i32.const 1)))
+ (br $next)))
+ (local.get $p))
+
+ ;; Compare two length-prefixed UTF-8 fields (exact equality or prefix).
+ (func $matches (param $a i32) (param $b i32) (param $prefix i32) (result i32)
+ (local $length i32) (local $i i32)
+ (local.set $length (i32.load (local.get $a)))
+ (if (i32.gt_u (local.get $length) (i32.load (local.get $b)))
+ (then (return (i32.const 0))))
+ (if (i32.and (i32.eqz (local.get $prefix))
+ (i32.ne (local.get $length) (i32.load (local.get $b))))
+ (then (return (i32.const 0))))
+ (block $done
+ (loop $next
+ (br_if $done (i32.eq (local.get $i) (local.get $length)))
+ (if (i32.ne
+ (i32.load8_u (i32.add (i32.add (local.get $a) (i32.const 4))
(local.get $i)))
+ (i32.load8_u (i32.add (i32.add (local.get $b) (i32.const 4))
(local.get $i))))
+ (then (return (i32.const 0))))
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
+ (br $next)))
+ (i32.const 1))
+
+ (func (export "doExecute") (param $id i64)
+ (local $length i32) (local $allow i32)
+ ;; Deliberately exercise the reference adapter's no-partial-write contract.
+ (if (i32.ne (call $get_args (local.get $id) (i64.const 16) (i32.const 1))
(i32.const -1))
+ (then unreachable))
+ (local.set $length (call $get_args (local.get $id) (i64.const 16)
(i32.const 65520)))
+ (if (i32.le_s (local.get $length) (i32.const 0)) (then unreachable))
+ (if (i32.ne (i32.load (i32.const 16)) (i32.const 1)) (then unreachable))
+ ;; Guest policy: request token equals rule handle AND selector handle
prefixes path.
+ (local.set $allow (i32.and
+ (call $matches (call $field (i32.const 3)) (call $field (i32.const 7))
(i32.const 0))
+ (call $matches (call $field (i32.const 1)) (call $field (i32.const 5))
(i32.const 1))))
+ ;; Return the decision followed by the original frame, proving what the
guest read.
+ (i32.store (i32.const 12) (i32.eqz (local.get $allow)))
+ (if (call $put_result (local.get $id) (i64.const 12)
+ (i32.add (local.get $length) (i32.const 4))) (then unreachable)))
+)
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/trap.wat
b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/trap.wat
new file mode 100644
index 0000000000..287e09416d
--- /dev/null
+++ b/shenyu-plugin/shenyu-plugin-wasm-base/src/test/wasm-gateway/trap.wat
@@ -0,0 +1,23 @@
+(;
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+;)
+
+(module
+ (import "shenyu" "get_args" (func $get_args (param i64 i64 i32) (result
i32)))
+ (memory (export "memory") 1)
+ (func (export "doExecute") (param $id i64)
+ (drop (call $get_args (local.get $id) (i64.const 16) (i32.const 65520)))
+ unreachable))