The SDK's host calls let a WASM filter reach outside its sandbox: dispatch HTTP and gRPC requests to upstream clusters, sleep without blocking a worker, and share state across plugin instances. Use them when a filter needs an external answer — a token check, a dynamic allowlist — but the filter logic itself should stay in-process.
HTTP callouts
dispatch_http_call sends a request to a named cluster from your configuration and returns the response. The call looks blocking in plugin code, but the module is suspended while it waits — proxy worker threads keep serving other traffic.
use arion_wasm_sdk::{bytes, dispatch_http_call, http};
let req = http::Request::builder()
.method(http::Method::POST)
.uri("/verify")
.body(bytes::Bytes::from("token=xyz"))
.unwrap();
match dispatch_http_call("auth", req) {
Ok(resp) if resp.status().is_success() => FilterAction::Continue,
_ => ctx.direct_response(deny_response()),
}gRPC callouts
dispatch_grpc_call speaks native gRPC to a cluster. You name the service and method, attach metadata, and pass the encoded protobuf message; the response carries the payload, metadata, and gRPC status (0 is OK). This needs the arion-wasm-types and smol_str crates in your plugin's dependencies.
use arion_wasm_sdk::dispatch_grpc_call;
use arion_wasm_types::GrpcCalloutRequest;
use smol_str::SmolStr;
let call = GrpcCalloutRequest {
cluster_name: SmolStr::new("auth"),
service_name: SmolStr::new("auth.AuthService"),
method_name: SmolStr::new("VerifyToken"),
initial_metadata: vec![],
message: encoded_request,
};
match dispatch_grpc_call(&call) {
Ok(resp) if resp.status == 0 => FilterAction::Continue,
_ => ctx.direct_response(deny_response()),
}Timers and deadlines
sleep(Duration) suspends the module without blocking the worker thread. set_io_timeout(Duration) arms a deadline for the callouts that follow: a callout that overruns it returns Err(ArionWasmError::Timeout) instead of hanging the request. Disarm it with clear_io_timeout() when the I/O is done.
use std::time::Duration;
use arion_wasm_sdk::{clear_io_timeout, set_io_timeout, ArionWasmError};
let _ = set_io_timeout(Duration::from_secs(2));
match dispatch_http_call("auth", req) {
Err(ArionWasmError::Timeout) => return ctx.direct_response(deny_response()),
other => handle(other),
}
let _ = clear_io_timeout();Shared state across instances
Each in-flight request runs its own plugin instance, so struct fields are request-local. For state that must survive across requests and workers, the host keeps named variables shared by every instance of the same configured filter (up to 1024 names per filter), in two shapes.
Atomics. SharedAtomicU64 and SharedAtomicI64 mirror std::sync::atomic: load, store, swap, compare_exchange, the fetch_* family, and fetch_update for bounded counters.
use arion_wasm_sdk::shared::SharedAtomicU64;
use std::sync::atomic::Ordering;
let counter = SharedAtomicU64::try_new("requests_total")?;
let n = counter.fetch_add(1, Ordering::SeqCst) + 1;
Versioned blobs. SharedBlob holds an arbitrary byte buffer guarded by a monotonic version. read() returns the data with its version; compare_and_swap(data, expected_version) commits only if nobody wrote in between — retry on failure.
use arion_wasm_sdk::shared::SharedBlob;
let blob = SharedBlob::try_new("allowlist")?;
loop {
let current = blob.read();
let updated = revise(¤t.data);
if blob.compare_and_swap(&updated, current.version).is_ok() {
break; // committed
} // lost the race: re-read and retry
}
Plugins can also push telemetry from a hook — set_custom_metrics for key/value metrics and set_access_log_operators for fields in access logs.
Callout or ext_proc?
A callout keeps the decision in your filter and asks a service one question per request; the filter still runs in-process and controls the outcome. External processing hands whole request and response phases to the service instead. Prefer a callout for small lookups feeding in-process logic; prefer ext_proc when the external service should own the transformation, is written in another language, or does work too heavy for the request path sandbox.
Limits
- Callout targets must be clusters defined in your configuration; arbitrary URLs are not dialable.
- Shared variables live in proxy memory: they are scoped to one filter on one Arion instance, and reset on restart.
- Without
set_io_timeout, a callout waits as long as the cluster takes; set a deadline for anything on the request path.