Class WasmRuntime

java.lang.Object
org.mockserver.wasm.WasmRuntime

public class WasmRuntime extends Object
Thin wrapper around a compiled chicory WASM instance.

Thread-safety: chicory Instance is NOT thread-safe, so a fresh Instance is created for each invocation. The parsed WasmModule, by contrast, is immutable and freely reusable across threads, so it is cached (see MODULE_CACHE) keyed by a content hash of the module bytes — parsing/validating the binary is chicory's most expensive step and is pure given the bytes, so it is done at most once per distinct module.

ABI. Two export shapes are supported, both returning non-zero for a match:

  • Legacy body-onlymatch(i32 ptr, i32 len) -> i32. The request body is written into linear memory at offset 0 and the function is called with (0, bodyLength).
  • Richer request envelopematch_request(i32 ptr, i32 len) -> i32. A JSON envelope {"version","method","path","queryStringParameters","headers","cookies","body"} is written into linear memory at offset 0 and the function is called with (0, jsonLength). This lets a module read the method, path, query parameters, headers and cookies in addition to the body. See ENVELOPE_VERSION for how the envelope stays backward compatible.
If the module exports match_request it is preferred; otherwise the runtime falls back to match with the body only, so existing body-only modules keep working unchanged.

Execution budget. A WASM module is user-supplied code with unrestricted control flow, so nothing in the module itself bounds how long an invocation runs — a module containing an unbounded loop would otherwise pin the calling thread forever. Because callMatch(WasmRequest) runs during request matching, that would wedge matcher threads and starve the event loop. Every invocation is therefore run on a shared daemon worker under a wall-clock budget (mockserver.wasmExecutionTimeoutMillis, default 5000ms; 0 disables the budget and runs inline on the calling thread). On expiry the worker is interrupted — chicory's interpreter polls Thread.isInterrupted() and unwinds with a ChicoryInterruptedException — and the call fails closed without waiting for the worker to die, so a module that somehow ignores interruption still cannot delay the caller.

This class fails closed: any error returns false (callMatch(java.lang.String)) or leaves the response unshaped (callShape(org.mockserver.wasm.WasmRequest, org.mockserver.wasm.WasmResponse)). The boundary catches Throwable, not merely Exception, because the guest is untrusted code whose failure modes are not all Exceptions. Chicory happens to convert a guest StackOverflowError into a ChicoryException at its call boundary, but that is chicory's implementation detail and covers only stack exhaustion inside a guest call — an OutOfMemoryError from an allocating module, or stack exhaustion in the surrounding host code (envelope serialisation, module parsing), is not wrapped. Letting any of those escape into the matcher would break the fail-closed guarantee precisely when a hostile module is exercising it. Errors caught here are logged at WARN rather than swallowed silently, so a genuine JVM-level problem stays diagnosable.

  • Constructor Details

    • WasmRuntime

      public WasmRuntime(byte[] wasmBytes)
      Create a runtime with the default memory page and execution budget limits from ConfigurationProperties.wasmMaxMemoryPages() and ConfigurationProperties.wasmExecutionTimeoutMillis().
    • WasmRuntime

      public WasmRuntime(byte[] wasmBytes, Configuration configuration)
      Create a runtime whose limits come from the LIVE Configuration rather than the static property store.

      This is the constructor production call sites must use. Reading the static store instead means a limit set through a Configuration instance or PUT /mockserver/configuration is accepted — the endpoint returns 200 and echoes the new value back — while the enforcement point keeps using the old one. A null configuration falls back to the static store, which is the correct source when no instance is in scope.

      Parameters:
      wasmBytes - the compiled WASM binary
      configuration - the live configuration supplying the memory-page and execution-budget limits
    • WasmRuntime

      public WasmRuntime(byte[] wasmBytes, int maxMemoryPages)
      Create a runtime with an explicit memory page limit and the default execution budget from ConfigurationProperties.wasmExecutionTimeoutMillis().
      Parameters:
      wasmBytes - the compiled WASM binary
      maxMemoryPages - maximum number of WASM linear memory pages (each page is 64 KiB)
    • WasmRuntime

      public WasmRuntime(byte[] wasmBytes, int maxMemoryPages, long executionTimeoutMillis)
      Create a runtime with explicit memory page and execution budget limits.
      Parameters:
      wasmBytes - the compiled WASM binary
      maxMemoryPages - maximum number of WASM linear memory pages (each page is 64 KiB)
      executionTimeoutMillis - maximum wall-clock milliseconds a single invocation may run before it is interrupted and fails closed; 0 or negative disables the budget and runs the invocation inline on the calling thread
  • Method Details

    • callMatch

      public boolean callMatch(String requestBody)
      Call the WASM module with just the request body (legacy body-only ABI).

      Retained for back-compat; equivalent to callMatch(WasmRequest.ofBody(requestBody)).

      Parameters:
      requestBody - the HTTP request body (may be null)
      Returns:
      true if the module reports a match
    • callMatch

      public boolean callMatch(WasmRequest request)
      Call the WASM module with the full request envelope (method, path, headers, body).

      If the module exports MATCH_REQUEST the JSON envelope is passed and that function is invoked; otherwise the runtime falls back to the legacy MATCH export with only the body, preserving back-compat for body-only modules.

      Parameters:
      request - the request parts to expose to the module (must not be null)
      Returns:
      true if the module reports a match
    • callShape

      public WasmResponse callShape(WasmRequest request, WasmResponse response)
      Call the module's optional SHAPE_RESPONSE export to (possibly) rewrite the response the matched expectation would return. This is the ABI v3 response-shaping hook.

      The runtime writes the shape envelope into linear memory at offset 0 and calls shape_response(0, len) -> i64. The module writes its response JSON somewhere in its own linear memory and returns a packed (ptr << 32) | len; the runtime reads len bytes at ptr and parses them into a WasmResponse. A return of 0 means "no change".

      Parameters:
      request - the matched request parts (serialised under the envelope's request field)
      response - the response the expectation would return (serialised under response)
      Returns:
      the parsed shaped response, or null when the module does not export SHAPE_RESPONSE or explicitly opts out (returns 0)
      Throws:
      WasmShapeException - if the module traps, returns an out-of-bounds or oversized region, or returns bytes that are not a valid response JSON object — the caller falls back to the unshaped response and logs once (see WasmResponseShaper)
    • invalidate

      public static void invalidate(byte[] wasmBytes)
      Drop the cached parsed module for the given bytes, if present. Called when a module is unloaded so its parsed form is released promptly. A no-op when the bytes were never cached. Correctness never depends on this (the cache is content-keyed); it only bounds memory.
    • invalidateAll

      public static void invalidateAll()
      Clear all cached parsed modules. Called on a full WASM store reset.