feat: add higress wasm go plugin development skill for Claude (#3402)

This commit is contained in:
澄潭
2026-01-28 19:08:10 +08:00
committed by GitHub
parent f2fcd68ef8
commit 2fe324761d
6 changed files with 1089 additions and 1 deletions

View File

@@ -0,0 +1,251 @@
---
name: higress-wasm-go-plugin
description: Develop Higress WASM plugins using Go 1.24+. Use when creating, modifying, or debugging Higress gateway plugins for HTTP request/response processing, external service calls, Redis integration, or custom gateway logic.
---
# Higress WASM Go Plugin Development
Develop Higress gateway WASM plugins using Go language with the `wasm-go` SDK.
## Quick Start
### Project Setup
```bash
# Create project directory
mkdir my-plugin && cd my-plugin
# Initialize Go module
go mod init my-plugin
# Set proxy (China)
go env -w GOPROXY=https://proxy.golang.com.cn,direct
# Download dependencies
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24
go get github.com/higress-group/wasm-go@main
go get github.com/tidwall/gjson
```
### Minimal Plugin Template
```go
package main
import (
"github.com/higress-group/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {}
func init() {
wrapper.SetCtx(
"my-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
)
}
type MyConfig struct {
Enabled bool
}
func parseConfig(json gjson.Result, config *MyConfig) error {
config.Enabled = json.Get("enabled").Bool()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
if config.Enabled {
proxywasm.AddHttpRequestHeader("x-my-header", "hello")
}
return types.HeaderContinue
}
```
### Compile
```bash
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
```
## Core Concepts
### Plugin Lifecycle
1. **init()** - Register plugin with `wrapper.SetCtx()`
2. **parseConfig** - Parse YAML config (auto-converted to JSON)
3. **HTTP processing phases** - Handle requests/responses
### HTTP Processing Phases
| Phase | Trigger | Handler |
|-------|---------|---------|
| Request Headers | Gateway receives client request headers | `ProcessRequestHeaders` |
| Request Body | Gateway receives client request body | `ProcessRequestBody` |
| Response Headers | Gateway receives backend response headers | `ProcessResponseHeaders` |
| Response Body | Gateway receives backend response body | `ProcessResponseBody` |
| Stream Done | HTTP stream completes | `ProcessStreamDone` |
### Action Return Values
| Action | Behavior |
|--------|----------|
| `types.HeaderContinue` | Continue to next filter |
| `types.HeaderStopIteration` | Stop header processing, wait for body |
| `types.HeaderStopAllIterationAndWatermark` | Stop all processing, buffer data, call `proxywasm.ResumeHttpRequest/Response()` to resume |
## API Reference
### HttpContext Methods
```go
// Request info (cached, safe to call in any phase)
ctx.Scheme() // :scheme
ctx.Host() // :authority
ctx.Path() // :path
ctx.Method() // :method
// Body handling
ctx.HasRequestBody() // Check if request has body
ctx.HasResponseBody() // Check if response has body
ctx.DontReadRequestBody() // Skip reading request body
ctx.DontReadResponseBody() // Skip reading response body
ctx.BufferRequestBody() // Buffer instead of stream
ctx.BufferResponseBody() // Buffer instead of stream
// Content detection
ctx.IsWebsocket() // Check WebSocket upgrade
ctx.IsBinaryRequestBody() // Check binary content
ctx.IsBinaryResponseBody() // Check binary content
// Context storage
ctx.SetContext(key, value)
ctx.GetContext(key)
ctx.GetStringContext(key, defaultValue)
ctx.GetBoolContext(key, defaultValue)
// Custom logging
ctx.SetUserAttribute(key, value)
ctx.WriteUserAttributeToLog()
```
### Header/Body Operations (proxywasm)
```go
// Request headers
proxywasm.GetHttpRequestHeader(name)
proxywasm.AddHttpRequestHeader(name, value)
proxywasm.ReplaceHttpRequestHeader(name, value)
proxywasm.RemoveHttpRequestHeader(name)
proxywasm.GetHttpRequestHeaders()
proxywasm.ReplaceHttpRequestHeaders(headers)
// Response headers
proxywasm.GetHttpResponseHeader(name)
proxywasm.AddHttpResponseHeader(name, value)
proxywasm.ReplaceHttpResponseHeader(name, value)
proxywasm.RemoveHttpResponseHeader(name)
proxywasm.GetHttpResponseHeaders()
proxywasm.ReplaceHttpResponseHeaders(headers)
// Request body (only in body phase)
proxywasm.GetHttpRequestBody(start, size)
proxywasm.ReplaceHttpRequestBody(body)
proxywasm.AppendHttpRequestBody(data)
proxywasm.PrependHttpRequestBody(data)
// Response body (only in body phase)
proxywasm.GetHttpResponseBody(start, size)
proxywasm.ReplaceHttpResponseBody(body)
proxywasm.AppendHttpResponseBody(data)
proxywasm.PrependHttpResponseBody(data)
// Direct response
proxywasm.SendHttpResponse(statusCode, headers, body, grpcStatus)
// Flow control
proxywasm.ResumeHttpRequest() // Resume paused request
proxywasm.ResumeHttpResponse() // Resume paused response
```
## Common Patterns
### External HTTP Call
See [references/http-client.md](references/http-client.md) for complete HTTP client patterns.
```go
func parseConfig(json gjson.Result, config *MyConfig) error {
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
err := config.client.Get("/api/check", nil, func(statusCode int, headers http.Header, body []byte) {
if statusCode != 200 {
proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
return
}
proxywasm.ResumeHttpRequest()
}, 3000) // timeout ms
if err != nil {
return types.HeaderContinue // fallback on error
}
return types.HeaderStopAllIterationAndWatermark
}
```
### Redis Integration
See [references/redis-client.md](references/redis-client.md) for complete Redis patterns.
```go
func parseConfig(json gjson.Result, config *MyConfig) error {
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: json.Get("redisService").String(),
Port: json.Get("redisPort").Int(),
})
return config.redis.Init(
json.Get("username").String(),
json.Get("password").String(),
json.Get("timeout").Int(),
)
}
```
### Multi-level Config
插件配置支持在控制台不同级别设置:全局、域名级、路由级。控制面会自动处理配置的优先级和匹配逻辑,插件代码中通过 `parseConfig` 解析到的就是当前请求匹配到的配置。
## Local Testing
See [references/local-testing.md](references/local-testing.md) for Docker Compose setup.
## Advanced Topics
See [references/advanced-patterns.md](references/advanced-patterns.md) for:
- Streaming body processing
- Route call pattern
- Tick functions (periodic tasks)
- Leader election
- Memory management
- Custom logging
## Best Practices
1. **Never call Resume after SendHttpResponse** - Response auto-resumes
2. **Check HasRequestBody() before returning HeaderStopIteration** - Avoids blocking
3. **Use cached ctx methods** - `ctx.Path()` works in any phase, `GetHttpRequestHeader(":path")` only in header phase
4. **Handle external call failures gracefully** - Return `HeaderContinue` on error to avoid blocking
5. **Set appropriate timeouts** - Default HTTP call timeout is 500ms

View File

@@ -0,0 +1,253 @@
# Advanced Patterns
## Streaming Body Processing
Process body chunks as they arrive without buffering:
```go
func init() {
wrapper.SetCtx(
"streaming-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessStreamingRequestBody(onStreamingRequestBody),
wrapper.ProcessStreamingResponseBody(onStreamingResponseBody),
)
}
func onStreamingRequestBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
// Modify chunk and return
modified := bytes.ReplaceAll(chunk, []byte("old"), []byte("new"))
return modified
}
func onStreamingResponseBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
// Can call external services with NeedPauseStreamingResponse()
return chunk
}
```
## Buffered Body Processing
Buffer entire body before processing:
```go
func init() {
wrapper.SetCtx(
"buffered-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestBody(onRequestBody),
wrapper.ProcessResponseBody(onResponseBody),
)
}
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
// Full request body available
var data map[string]interface{}
json.Unmarshal(body, &data)
// Modify and replace
data["injected"] = "value"
newBody, _ := json.Marshal(data)
proxywasm.ReplaceHttpRequestBody(newBody)
return types.ActionContinue
}
```
## Route Call Pattern
Call the current route's upstream with modified request:
```go
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
err := ctx.RouteCall("POST", "/modified-path", [][2]string{
{"Content-Type", "application/json"},
{"X-Custom", "header"},
}, body, func(statusCode int, headers [][2]string, body []byte) {
// Handle response from upstream
proxywasm.SendHttpResponse(statusCode, headers, body, -1)
})
if err != nil {
proxywasm.SendHttpResponse(500, nil, []byte("Route call failed"), -1)
}
return types.ActionContinue
}
```
## Tick Functions (Periodic Tasks)
Register periodic background tasks:
```go
func parseConfig(json gjson.Result, config *MyConfig) error {
// Register tick functions during config parsing
wrapper.RegisterTickFunc(1000, func() {
// Executes every 1 second
log.Info("1s tick")
})
wrapper.RegisterTickFunc(5000, func() {
// Executes every 5 seconds
log.Info("5s tick")
})
return nil
}
```
## Leader Election
For tasks that should run on only one VM instance:
```go
func init() {
wrapper.SetCtx(
"leader-plugin",
wrapper.PrePluginStartOrReload(onPluginStart),
wrapper.ParseConfig(parseConfig),
)
}
func onPluginStart(ctx wrapper.PluginContext) error {
ctx.DoLeaderElection()
return nil
}
func parseConfig(json gjson.Result, config *MyConfig) error {
wrapper.RegisterTickFunc(10000, func() {
if ctx.IsLeader() {
// Only leader executes this
log.Info("Leader task")
}
})
return nil
}
```
## Plugin Context Storage
Store data across requests at plugin level:
```go
type MyConfig struct {
// Config fields
}
func init() {
wrapper.SetCtx(
"context-plugin",
wrapper.ParseConfigWithContext(parseConfigWithContext),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
)
}
func parseConfigWithContext(ctx wrapper.PluginContext, json gjson.Result, config *MyConfig) error {
// Store in plugin context (survives across requests)
ctx.SetContext("initTime", time.Now().Unix())
return nil
}
```
## Rule-Level Config Isolation
Enable graceful degradation when rule config parsing fails:
```go
func init() {
wrapper.SetCtx(
"isolated-plugin",
wrapper.PrePluginStartOrReload(func(ctx wrapper.PluginContext) error {
ctx.EnableRuleLevelConfigIsolation()
return nil
}),
wrapper.ParseOverrideConfig(parseGlobal, parseRule),
)
}
func parseGlobal(json gjson.Result, config *MyConfig) error {
// Parse global config
return nil
}
func parseRule(json gjson.Result, global MyConfig, config *MyConfig) error {
// Parse per-rule config, inheriting from global
*config = global // Copy global defaults
// Override with rule-specific values
return nil
}
```
## Memory Management
Configure automatic VM rebuild to prevent memory leaks:
```go
func init() {
wrapper.SetCtxWithOptions(
"memory-managed-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.WithRebuildAfterRequests(10000), // Rebuild after 10k requests
wrapper.WithRebuildMaxMemBytes(100*1024*1024), // Rebuild at 100MB
wrapper.WithMaxRequestsPerIoCycle(20), // Limit concurrent requests
)
}
```
## Custom Logging
Add structured fields to access logs:
```go
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Set custom attributes
ctx.SetUserAttribute("user_id", "12345")
ctx.SetUserAttribute("request_type", "api")
return types.HeaderContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Write to access log
ctx.WriteUserAttributeToLog()
// Or write to trace spans
ctx.WriteUserAttributeToTrace()
return types.HeaderContinue
}
```
## Disable Re-routing
Prevent Envoy from recalculating routes after header modification:
```go
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Call BEFORE modifying headers
ctx.DisableReroute()
// Now safe to modify headers without triggering re-route
proxywasm.ReplaceHttpRequestHeader(":path", "/new-path")
return types.HeaderContinue
}
```
## Buffer Limits
Set per-request buffer limits to control memory usage:
```go
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Allow larger request bodies for this request
ctx.SetRequestBodyBufferLimit(10 * 1024 * 1024) // 10MB
return types.HeaderContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Allow larger response bodies
ctx.SetResponseBodyBufferLimit(50 * 1024 * 1024) // 50MB
return types.HeaderContinue
}
```

View File

@@ -0,0 +1,179 @@
# HTTP Client Reference
## Cluster Types
### FQDNCluster (Most Common)
For services registered in Higress with FQDN:
```go
wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: "my-service.dns", // Service FQDN with suffix
Port: 8080,
Host: "optional-host-header", // Optional
})
```
Common FQDN suffixes:
- `.dns` - DNS service
- `.static` - Static IP service (port defaults to 80)
- `.nacos` - Nacos service
### K8sCluster
For Kubernetes services:
```go
wrapper.NewClusterClient(wrapper.K8sCluster{
ServiceName: "my-service",
Namespace: "default",
Port: 8080,
Version: "", // Optional subset version
})
// Generates: outbound|8080||my-service.default.svc.cluster.local
```
### NacosCluster
For Nacos registry services:
```go
wrapper.NewClusterClient(wrapper.NacosCluster{
ServiceName: "my-service",
Group: "DEFAULT-GROUP",
NamespaceID: "public",
Port: 8080,
IsExtRegistry: false, // true for EDAS/SAE
})
```
### StaticIpCluster
For static IP services:
```go
wrapper.NewClusterClient(wrapper.StaticIpCluster{
ServiceName: "my-service",
Port: 8080,
})
// Generates: outbound|8080||my-service.static
```
### DnsCluster
For DNS-resolved services:
```go
wrapper.NewClusterClient(wrapper.DnsCluster{
ServiceName: "my-service",
Domain: "api.example.com",
Port: 443,
})
```
### RouteCluster
Use current route's upstream:
```go
wrapper.NewClusterClient(wrapper.RouteCluster{
Host: "optional-host-override",
})
```
### TargetCluster
Direct cluster name specification:
```go
wrapper.NewClusterClient(wrapper.TargetCluster{
Cluster: "outbound|8080||my-service.dns",
Host: "api.example.com",
})
```
## HTTP Methods
```go
client.Get(path, headers, callback, timeout...)
client.Post(path, headers, body, callback, timeout...)
client.Put(path, headers, body, callback, timeout...)
client.Patch(path, headers, body, callback, timeout...)
client.Delete(path, headers, body, callback, timeout...)
client.Head(path, headers, callback, timeout...)
client.Options(path, headers, callback, timeout...)
client.Call(method, path, headers, body, callback, timeout...)
```
## Callback Signature
```go
func(statusCode int, responseHeaders http.Header, responseBody []byte)
```
## Complete Example
```go
type MyConfig struct {
client wrapper.HttpClient
requestPath string
tokenHeader string
}
func parseConfig(json gjson.Result, config *MyConfig) error {
config.tokenHeader = json.Get("tokenHeader").String()
if config.tokenHeader == "" {
return errors.New("missing tokenHeader")
}
config.requestPath = json.Get("requestPath").String()
if config.requestPath == "" {
return errors.New("missing requestPath")
}
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
if strings.HasSuffix(serviceName, ".static") {
servicePort = 80
}
}
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
err := config.client.Get(config.requestPath, nil,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
if statusCode != http.StatusOK {
log.Errorf("http call failed, status: %d", statusCode)
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
[]byte("http call failed"), -1)
return
}
token := responseHeaders.Get(config.tokenHeader)
if token != "" {
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
}
proxywasm.ResumeHttpRequest()
})
if err != nil {
log.Errorf("http call dispatch failed: %v", err)
return types.HeaderContinue
}
return types.HeaderStopAllIterationAndWatermark
}
```
## Important Notes
1. **Cannot use net/http** - Must use wrapper's HTTP client
2. **Default timeout is 500ms** - Pass explicit timeout for longer calls
3. **Callback is async** - Must return `HeaderStopAllIterationAndWatermark` and call `ResumeHttpRequest()` in callback
4. **Error handling** - If dispatch fails, return `HeaderContinue` to avoid blocking

View File

@@ -0,0 +1,189 @@
# Local Testing with Docker Compose
## Prerequisites
- Docker installed
- Compiled `main.wasm` file
## Setup
Create these files in your plugin directory:
### docker-compose.yaml
```yaml
version: '3.7'
services:
envoy:
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
entrypoint: /usr/local/bin/envoy
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
depends_on:
- httpbin
networks:
- wasmtest
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./main.wasm:/etc/envoy/main.wasm
httpbin:
image: kennethreitz/httpbin:latest
networks:
- wasmtest
ports:
- "12345:80"
networks:
wasmtest: {}
```
### envoy.yaml
```yaml
admin:
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
scheme_header_transformation:
scheme_to_overwrite: https
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: httpbin
http_filters:
- name: wasmdemo
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: wasmdemo
vm_config:
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/main.wasm
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": false
}
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: httpbin
connect_timeout: 30s
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 80
```
## Running
```bash
# Start
docker compose up
# Test without gateway (baseline)
curl http://127.0.0.1:12345/get
# Test with gateway (plugin applied)
curl http://127.0.0.1:10000/get
# Stop
docker compose down
```
## Modifying Plugin Config
1. Edit the `configuration.value` section in `envoy.yaml`
2. Restart: `docker compose restart envoy`
## Viewing Logs
```bash
# Follow Envoy logs
docker compose logs -f envoy
# WASM debug logs (enabled by --component-log-level wasm:debug)
```
## Adding External Services
To test external HTTP/Redis calls, add services to docker-compose.yaml:
```yaml
services:
# ... existing services ...
redis:
image: redis:7-alpine
networks:
- wasmtest
ports:
- "6379:6379"
auth-service:
image: your-auth-service:latest
networks:
- wasmtest
```
Then add clusters to envoy.yaml:
```yaml
clusters:
# ... existing clusters ...
- name: outbound|6379||redis.static
connect_timeout: 5s
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: redis
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: redis
port_value: 6379
```

View File

@@ -0,0 +1,215 @@
# Redis Client Reference
## Initialization
```go
type MyConfig struct {
redis wrapper.RedisClient
qpm int
}
func parseConfig(json gjson.Result, config *MyConfig) error {
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
servicePort = 6379
}
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
return config.redis.Init(
json.Get("username").String(),
json.Get("password").String(),
json.Get("timeout").Int(), // milliseconds
// Optional settings:
// wrapper.WithDataBase(1),
// wrapper.WithBufferFlushTimeout(3*time.Millisecond),
// wrapper.WithMaxBufferSizeBeforeFlush(1024),
// wrapper.WithDisableBuffer(), // For latency-sensitive scenarios
)
}
```
## Callback Signature
```go
func(response resp.Value)
// Check for errors
if response.Error() != nil {
// Handle error
}
// Get values
response.Integer() // int
response.String() // string
response.Bool() // bool
response.Array() // []resp.Value
response.Bytes() // []byte
```
## Available Commands
### Key Operations
```go
redis.Del(key, callback)
redis.Exists(key, callback)
redis.Expire(key, ttlSeconds, callback)
redis.Persist(key, callback)
```
### String Operations
```go
redis.Get(key, callback)
redis.Set(key, value, callback)
redis.SetEx(key, value, ttlSeconds, callback)
redis.SetNX(key, value, ttlSeconds, callback) // ttl=0 means no expiry
redis.MGet(keys, callback)
redis.MSet(kvMap, callback)
redis.Incr(key, callback)
redis.Decr(key, callback)
redis.IncrBy(key, delta, callback)
redis.DecrBy(key, delta, callback)
```
### List Operations
```go
redis.LLen(key, callback)
redis.RPush(key, values, callback)
redis.RPop(key, callback)
redis.LPush(key, values, callback)
redis.LPop(key, callback)
redis.LIndex(key, index, callback)
redis.LRange(key, start, stop, callback)
redis.LRem(key, count, value, callback)
redis.LInsertBefore(key, pivot, value, callback)
redis.LInsertAfter(key, pivot, value, callback)
```
### Hash Operations
```go
redis.HExists(key, field, callback)
redis.HDel(key, fields, callback)
redis.HLen(key, callback)
redis.HGet(key, field, callback)
redis.HSet(key, field, value, callback)
redis.HMGet(key, fields, callback)
redis.HMSet(key, kvMap, callback)
redis.HKeys(key, callback)
redis.HVals(key, callback)
redis.HGetAll(key, callback)
redis.HIncrBy(key, field, delta, callback)
redis.HIncrByFloat(key, field, delta, callback)
```
### Set Operations
```go
redis.SCard(key, callback)
redis.SAdd(key, values, callback)
redis.SRem(key, values, callback)
redis.SIsMember(key, value, callback)
redis.SMembers(key, callback)
redis.SDiff(key1, key2, callback)
redis.SDiffStore(dest, key1, key2, callback)
redis.SInter(key1, key2, callback)
redis.SInterStore(dest, key1, key2, callback)
redis.SUnion(key1, key2, callback)
redis.SUnionStore(dest, key1, key2, callback)
```
### Sorted Set Operations
```go
redis.ZCard(key, callback)
redis.ZAdd(key, memberScoreMap, callback)
redis.ZCount(key, min, max, callback)
redis.ZIncrBy(key, member, delta, callback)
redis.ZScore(key, member, callback)
redis.ZRank(key, member, callback)
redis.ZRevRank(key, member, callback)
redis.ZRem(key, members, callback)
redis.ZRange(key, start, stop, callback)
redis.ZRevRange(key, start, stop, callback)
```
### Lua Script
```go
redis.Eval(script, numkeys, keys, args, callback)
```
### Raw Command
```go
redis.Command([]interface{}{"SET", "key", "value"}, callback)
```
## Rate Limiting Example
```go
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
now := time.Now()
minuteAligned := now.Truncate(time.Minute)
timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
err := config.redis.Incr(timeStamp, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("redis error: %v", response.Error())
proxywasm.ResumeHttpRequest()
return
}
count := response.Integer()
ctx.SetContext("timeStamp", timeStamp)
ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm - count))
if count == 1 {
// First request in this minute, set expiry
config.redis.Expire(timeStamp, 60, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("expire error: %v", response.Error())
}
proxywasm.ResumeHttpRequest()
})
} else if count > config.qpm {
proxywasm.SendHttpResponse(429, [][2]string{
{"timeStamp", timeStamp},
{"callTimeLeft", "0"},
}, []byte("Too many requests\n"), -1)
} else {
proxywasm.ResumeHttpRequest()
}
})
if err != nil {
log.Errorf("redis call failed: %v", err)
return types.HeaderContinue
}
return types.HeaderStopAllIterationAndWatermark
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
if ts := ctx.GetContext("timeStamp"); ts != nil {
proxywasm.AddHttpResponseHeader("timeStamp", ts.(string))
}
if left := ctx.GetContext("callTimeLeft"); left != nil {
proxywasm.AddHttpResponseHeader("callTimeLeft", left.(string))
}
return types.HeaderContinue
}
```
## Important Notes
1. **Check Ready()** - `redis.Ready()` returns false if init failed
2. **Auto-reconnect** - Client handles NOAUTH errors and re-authenticates automatically
3. **Buffering** - Default 3ms flush timeout and 1024 byte buffer; use `WithDisableBuffer()` for latency-sensitive scenarios
4. **Error handling** - Always check `response.Error()` in callbacks

View File

@@ -36,6 +36,7 @@ header:
- 'pkg/ingress/kube/gateway/istio/testdata' - 'pkg/ingress/kube/gateway/istio/testdata'
- 'release-notes/**' - 'release-notes/**'
- '.cursor/**' - '.cursor/**'
- '.claude/**'
comment: on-failure comment: on-failure
dependency: dependency: