Files
higress/.claude/skills/nginx-to-higress-migration/references/snippet-patterns.md

8.1 KiB

Common Nginx Snippet to WASM Plugin Patterns

Header Manipulation

Add Response Header

Nginx snippet:

more_set_headers "X-Custom-Header: custom-value";
more_set_headers "X-Request-ID: $request_id";

WASM plugin:

func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    proxywasm.AddHttpResponseHeader("X-Custom-Header", "custom-value")
    
    // For request ID, get from request context
    if reqId, err := proxywasm.GetHttpRequestHeader("x-request-id"); err == nil {
        proxywasm.AddHttpResponseHeader("X-Request-ID", reqId)
    }
    return types.HeaderContinue
}

Remove Headers

Nginx snippet:

more_clear_headers "Server";
more_clear_headers "X-Powered-By";

WASM plugin:

func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    proxywasm.RemoveHttpResponseHeader("Server")
    proxywasm.RemoveHttpResponseHeader("X-Powered-By")
    return types.HeaderContinue
}

Conditional Header

Nginx snippet:

if ($http_x_custom_flag = "enabled") {
    more_set_headers "X-Feature: active";
}

WASM plugin:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    flag, _ := proxywasm.GetHttpRequestHeader("x-custom-flag")
    if flag == "enabled" {
        proxywasm.AddHttpRequestHeader("X-Feature", "active")
    }
    return types.HeaderContinue
}

Request Validation

Block by Path Pattern

Nginx snippet:

if ($request_uri ~* "(\.php|\.asp|\.aspx)$") {
    return 403;
}

WASM plugin:

import "regexp"

type MyConfig struct {
    BlockPattern *regexp.Regexp
}

func parseConfig(json gjson.Result, config *MyConfig) error {
    pattern := json.Get("blockPattern").String()
    if pattern == "" {
        pattern = `\.(php|asp|aspx)$`
    }
    config.BlockPattern = regexp.MustCompile(pattern)
    return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    path := ctx.Path()
    if config.BlockPattern.MatchString(path) {
        proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
        return types.HeaderStopAllIterationAndWatermark
    }
    return types.HeaderContinue
}

Block by User Agent

Nginx snippet:

if ($http_user_agent ~* "(bot|crawler|spider)") {
    return 403;
}

WASM plugin:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    ua, _ := proxywasm.GetHttpRequestHeader("user-agent")
    ua = strings.ToLower(ua)
    
    blockedPatterns := []string{"bot", "crawler", "spider"}
    for _, pattern := range blockedPatterns {
        if strings.Contains(ua, pattern) {
            proxywasm.SendHttpResponse(403, nil, []byte("Blocked"), -1)
            return types.HeaderStopAllIterationAndWatermark
        }
    }
    return types.HeaderContinue
}

Request Size Validation

Nginx snippet:

if ($content_length > 10485760) {
    return 413;
}

WASM plugin:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    clStr, _ := proxywasm.GetHttpRequestHeader("content-length")
    if cl, err := strconv.ParseInt(clStr, 10, 64); err == nil {
        if cl > 10*1024*1024 { // 10MB
            proxywasm.SendHttpResponse(413, nil, []byte("Request too large"), -1)
            return types.HeaderStopAllIterationAndWatermark
        }
    }
    return types.HeaderContinue
}

Request Modification

URL Rewrite with Logic

Nginx snippet:

set $backend "default";
if ($http_x_version = "v2") {
    set $backend "v2";
}
rewrite ^/api/(.*)$ /api/$backend/$1 break;

WASM plugin:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    version, _ := proxywasm.GetHttpRequestHeader("x-version")
    backend := "default"
    if version == "v2" {
        backend = "v2"
    }
    
    path := ctx.Path()
    if strings.HasPrefix(path, "/api/") {
        newPath := "/api/" + backend + path[4:]
        proxywasm.ReplaceHttpRequestHeader(":path", newPath)
    }
    return types.HeaderContinue
}

Add Query Parameter

Nginx snippet:

if ($args !~ "source=") {
    set $args "${args}&source=gateway";
}

WASM plugin:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    path := ctx.Path()
    if !strings.Contains(path, "source=") {
        separator := "?"
        if strings.Contains(path, "?") {
            separator = "&"
        }
        newPath := path + separator + "source=gateway"
        proxywasm.ReplaceHttpRequestHeader(":path", newPath)
    }
    return types.HeaderContinue
}

Lua Script Conversion

Simple Lua Access Check

Nginx Lua:

access_by_lua_block {
    local token = ngx.var.http_authorization
    if not token or token == "" then
        ngx.exit(401)
    end
}

WASM plugin:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    token, _ := proxywasm.GetHttpRequestHeader("authorization")
    if token == "" {
        proxywasm.SendHttpResponse(401, [][2]string{
            {"WWW-Authenticate", "Bearer"},
        }, []byte("Unauthorized"), -1)
        return types.HeaderStopAllIterationAndWatermark
    }
    return types.HeaderContinue
}

Lua with Redis

Nginx Lua:

access_by_lua_block {
    local redis = require "resty.redis"
    local red = redis:new()
    red:connect("127.0.0.1", 6379)
    
    local ip = ngx.var.remote_addr
    local count = red:incr("rate:" .. ip)
    if count > 100 then
        ngx.exit(429)
    end
    red:expire("rate:" .. ip, 60)
}

WASM plugin:

// See references/redis-client.md in higress-wasm-go-plugin skill
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("redisPassword").String(), 1000)
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    ip, _ := proxywasm.GetHttpRequestHeader("x-real-ip")
    if ip == "" {
        ip, _ = proxywasm.GetHttpRequestHeader("x-forwarded-for")
    }
    
    key := "rate:" + ip
    err := config.redis.Incr(key, func(val int) {
        if val > 100 {
            proxywasm.SendHttpResponse(429, nil, []byte("Rate limited"), -1)
            return
        }
        config.redis.Expire(key, 60, nil)
        proxywasm.ResumeHttpRequest()
    })
    
    if err != nil {
        return types.HeaderContinue // Fallback on Redis error
    }
    return types.HeaderStopAllIterationAndWatermark
}

Response Modification

Inject Script/Content

Nginx snippet:

sub_filter '</head>' '<script src="/tracking.js"></script></head>';
sub_filter_once on;

WASM plugin:

func init() {
    wrapper.SetCtx(
        "inject-script",
        wrapper.ParseConfig(parseConfig),
        wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
        wrapper.ProcessResponseBody(onHttpResponseBody),
    )
}

func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
    if strings.Contains(contentType, "text/html") {
        ctx.BufferResponseBody()
        proxywasm.RemoveHttpResponseHeader("content-length")
    }
    return types.HeaderContinue
}

func onHttpResponseBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
    bodyStr := string(body)
    injection := `<script src="/tracking.js"></script></head>`
    newBody := strings.Replace(bodyStr, "</head>", injection, 1)
    proxywasm.ReplaceHttpResponseBody([]byte(newBody))
    return types.BodyContinue
}

Best Practices

  1. Error Handling: Always handle external call failures gracefully
  2. Performance: Cache regex patterns in config, avoid recompiling
  3. Timeout: Set appropriate timeouts for external calls (default 500ms)
  4. Logging: Use proxywasm.LogInfo/Warn/Error for debugging
  5. Testing: Test locally with Docker Compose before deploying