Todos los productos
Search
Centro de documentos

API Gateway:Develop gateway plugins in Go

Última actualización: Sep 03, 2026

Desarrolle y depure localmente plugins personalizados de gateway en Go para extender la funcionalidad del API gateway y cubrir requisitos empresariales complejos.

Importante

Higress migró de TinyGo 0.29 + Go 1.20 a la compilación nativa de Wasm en Go 1.24. Go 1.24 admite de forma nativa la compilación de archivos .wasm.

Para migrar de TinyGo a Go 1.24, traslade la lógica de inicialización de su plugin desde la función main hacia la función init y actualice las dependencias en su archivo go.mod. A continuación se proporciona un ejemplo.

Ajustes de compatibilidad para migrar plugins basados en TinyGo:

1. Si llama a un servicio externo durante el procesamiento de encabezados y devuelve type.ActionPause, reemplácelo por types.HeaderStopAllIterationAndWatermark. El ejemplo "Llamada a un servicio HTTP externo" que aparece más adelante ilustra este caso.

2. Si utilizaba go-re2 (necesario porque TinyGo tenía soporte incompleto para regexp), sustitúyalo por el paquete estándar regexp de Go.

Requisitos previos

Instale Go 1.24 o una versión posterior en su computadora de desarrollo.

Golang

Instale Go 1.24 o una versión posterior desde la guía de instalación oficial .

Nota

Los plugins de Go 1.24 requieren Cloud-native API Gateway 2.1.5 o superior. Para versiones anteriores del gateway, consulte Desarrollo de plugins WASM con el lenguaje Go.

Windows

  • Descargue el archivo de instalación.

  • Ejecute el instalador. De forma predeterminada, Go se instala en Program Files o Program Files (x86).

  • Presione Win+R, introduzca cmd y haga clic en OK para abrir el símbolo del sistema. Ejecute go version para comprobar la instalación.

macOS

  • Descargue el paquete de instalación.

  • Haga doble clic en el paquete para instalarlo. De forma predeterminada, Go se instala en /usr/local/go.

  • Abra una terminal y ejecute go version para comprobar la instalación.

Linux

  • Descargue el paquete de instalación.

  • Ejecute los siguientes comandos para instalar Go:

    • Instale Go.

      rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gz
    • Configure la variable de entorno.

      export PATH=$PATH:/usr/local/go/bin
    • Ejecute go version para comprobar la instalación.

Escritura del plugin

Inicialización del proyecto

  1. Cree un directorio de proyecto e inicialice un módulo de Go:

       mkdir wasm-demo-go && cd wasm-demo-go
       go mod init wasm-demo-go
  2. Descargue las dependencias del SDK del plugin:

       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

    Si se encuentra en China continental, configure primero un proxy:

       go env -w GOPROXY=https://proxy.golang.com.cn,direct

Escritura del archivo main.go

El siguiente ejemplo agrega un encabezado de solicitud hello: world a cada solicitud entrante. Cuando mockEnable es true en la configuración del plugin, este devuelve hello world directamente en lugar de reenviar la solicitud al backend.

package main

import (
  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
  "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(
    // Plugin name
    "my-plugin",
    // Custom configuration parser
     wrapper.ParseConfigBy(parseConfig),
    // Hook into the request header processing phase
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
  )
}

// Custom plugin configuration
type MyConfig struct {
  mockEnable bool
}

// Parse the JSON configuration into the config struct.
// The gateway console converts YAML to JSON automatically.
func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
  config.mockEnable = json.Get("mockEnable").Bool()
  return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  proxywasm.AddHttpRequestHeader("hello", "world")
  if config.mockEnable {
    proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
  }
  return types.HeaderContinue
}

Puntos clave:

  • La función main() debe estar vacía. Coloque toda la lógica de inicialización en init().

  • wrapper.SetCtx registra el nombre del plugin, el analizador de configuración y los hooks de procesamiento.

  • Devuelva types.HeaderContinue (equivalente a types.ActionContinue) para pasar la solicitud al siguiente filtro.

Referencia del SDK

Métodos de utilidad

El SDK del plugin proporciona los siguientes métodos de proxywasm para manipular solicitudes y respuestas:

Procesamiento de encabezados de solicitud (válido durante la fase de encabezados de solicitud)

Method

Purpose

GetHttpRequestHeaders

Obtiene todos los encabezados de solicitud

ReplaceHttpRequestHeaders

Reemplaza todos los encabezados de solicitud

GetHttpRequestHeader

Obtiene un encabezado de solicitud específico

RemoveHttpRequestHeader

Elimina un encabezado de solicitud específico

ReplaceHttpRequestHeader

Reemplaza un encabezado de solicitud específico

AddHttpRequestHeader

Agrega un encabezado de solicitud

Procesamiento del cuerpo de solicitud (válido durante la fase de cuerpo de solicitud)

Method

Purpose

GetHttpRequestBody

Obtiene el cuerpo de la solicitud

AppendHttpRequestBody

Anexa datos al final del cuerpo de la solicitud

PrependHttpRequestBody

Antepone datos al inicio del cuerpo de la solicitud

ReplaceHttpRequestBody

Reemplaza todo el cuerpo de la solicitud

Procesamiento de encabezados de respuesta (válido durante la fase de encabezados de respuesta)

Method

Purpose

GetHttpResponseHeaders

Obtiene todos los encabezados de respuesta del backend

ReplaceHttpResponseHeaders

Reemplaza todos los encabezados de respuesta

GetHttpResponseHeader

Obtiene un encabezado de respuesta específico

RemoveHttpResponseHeader

Elimina un encabezado de respuesta específico

ReplaceHttpResponseHeader

Reemplaza un encabezado de respuesta específico

AddHttpResponseHeader

Agrega un encabezado de respuesta

Procesamiento del cuerpo de respuesta (válido durante la fase de cuerpo de respuesta)

Method

Purpose

GetHttpResponseBody

Obtiene el cuerpo de la respuesta

AppendHttpResponseBody

Anexa datos al final del cuerpo de la respuesta

PrependHttpResponseBody

Antepone datos al inicio del cuerpo de la respuesta

ReplaceHttpResponseBody

Reemplaza todo el cuerpo de la respuesta

Llamadas HTTP y control de flujo

Method

Purpose

DispatchHttpCall

Envía una solicitud HTTP a un servicio externo

GetHttpCallResponseHeaders

Obtiene los encabezados de respuesta de una solicitud DispatchHttpCall

GetHttpCallResponseBody

Obtiene el cuerpo de respuesta de una solicitud DispatchHttpCall

GetHttpCallResponseTrailers

Obtiene los trailers de respuesta de una solicitud DispatchHttpCall

SendHttpResponse

Devuelve una respuesta HTTP directamente al cliente

ResumeHttpRequest

Reanuda un flujo de procesamiento de solicitud pausado

ResumeHttpResponse

Reanuda un flujo de procesamiento de respuesta pausado

Importante

No llame a ResumeHttpRequest ni a ResumeHttpResponse cuando el procesamiento no esté pausado. Después de llamar a SendHttpResponse, cualquier solicitud o respuesta pausada se reanuda automáticamente. Llamar nuevamente a ResumeHttpRequest o ResumeHttpResponse provoca un comportamiento indefinido.

Códigos de estado de encabezados

Cada hook de procesamiento devuelve un código de estado de encabezado que controla el flujo de la solicitud. Elija el estado adecuado según si su plugin necesita pausar, almacenar en búfer o transmitir datos:

Status

Behavior

HeaderContinue

El filtro actual finalizó. Pasa la solicitud al siguiente filtro. Equivalente a types.ActionContinue.

HeaderStopIteration

Retiene el encabezado pero continúa leyendo los datos del cuerpo. Utilícelo para modificar encabezados de solicitud durante la fase de procesamiento del cuerpo. Se requiere un cuerpo; si no existe, la solicitud se bloquea indefinidamente. Compruébelo con HasRequestBody().

HeaderContinueAndEndStream

Pasa el encabezado al siguiente filtro con end_stream = false, lo que permite al filtro actual anexar más datos al cuerpo.

HeaderStopAllIterationAndBuffer

Detiene toda iteración y almacena en búfer encabezados, cuerpo y trailers. Si el búfer supera el límite, el gateway devuelve 413 durante la fase de solicitud o 500 durante la fase de respuesta. Reanude con proxywasm.ResumeHttpRequest(), proxywasm.ResumeHttpResponse() o proxywasm.SendHttpResponseWithDetail().

HeaderStopAllIterationAndWatermark

Igual que HeaderStopAllIterationAndBuffer, pero activa limitación a nivel de conexión cuando el búfer supera el límite en lugar de devolver un error. Equivalente a types.ActionPause en ABI 0.2.1.

Nota

Para ver ejemplos prácticos de HeaderStopIteration y HeaderStopAllIterationAndWatermark, consulte el plugin ai-transformer y el plugin ai-quota de Higress.

Compilación del archivo Wasm

Compile el plugin en un binario Wasm:

go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./

Esto genera un archivo main.wasm. Utilice este archivo para depuración local o cárguelo mediante el marketplace del gateway cloud-native para desplegar un plugin personalizado.

Para desplegar mediante Higress con la definición de recurso personalizado (CRD) WasmPlugin o la interfaz de usuario de la consola, empaquete el archivo Wasm en una imagen OCI o Docker. Para más detalles, consulte Plugins personalizados.

Depuración local

Requisitos previos

Instale Docker.

Configuración del entorno de prueba

Asegúrese de que main.wasm exista en el directorio de su proyecto y cree los dos archivos siguientes.

docker-compose.yaml

version: '3.7'
services:
  envoy:
    image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
    entrypoint: /usr/local/bin/envoy
    # Enable debug-level logging for Wasm. Use info level in production.
    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

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
    # Comment out the following line to test on v6 networks
    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

Inicie el entorno:

docker compose up

Verificación

Pruebe el encabezado agregado. Envíe una solicitud a través del gateway (puerto 10000) y confirme que aparece el encabezado Hello: world:

curl http://127.0.0.1:10000/get

Respuesta esperada:

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Hello": "world",
    "Host": "127.0.0.1:10000",
    "Original-Host": "127.0.0.1:10000",
    "Req-Start-Time": "1681269273896",
    "User-Agent": "curl/7.79.1",
    "X-Envoy-Expected-Rq-Timeout-Ms": "15000"
  },
  "origin": "172.18.0.3",
  "url": "https://127.0.0.1:10000/get"
}

El encabezado Hello: world confirma que el plugin está activo.

Como comparación, una solicitud directa a httpbin (puerto 12345) no incluye este encabezado:

curl http://127.0.0.1:12345/get

Pruebe un cambio de configuración. Edite envoy.yaml y establezca mockEnable en true:

configuration:
    "@type": "type.googleapis.com/google.protobuf.StringValue"
    value: |
      {
        "mockEnable": true
      }

Reinicie el entorno y envíe la misma solicitud:

curl http://127.0.0.1:10000/get

Respuesta esperada:

hello world

La respuesta simulada confirma que los cambios de configuración surten efecto correctamente.

Más ejemplos

Plugin sin configuración

Para un plugin que no requiere configuración, defina una estructura de configuración vacía y omita ParseConfigBy:

package main

import (
  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
)

func main() {}

func init() {
  wrapper.SetCtx(
    "hello-world",
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
  )
}

type MyConfig struct {}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
  return types.HeaderContinue
}

Llamada a un servicio HTTP externo

Los plugins admiten llamadas HTTP a servicios Nacos, servicios Kubernetes y servicios de dirección fija o DNS configurados en la consola del gateway. La biblioteca estándar net/http no está disponible en el entorno de ejecución Wasm. Utilice en su lugar el cliente HTTP encapsulado del SDK.

El siguiente ejemplo analiza la configuración del servicio durante el inicio y luego llama al servicio durante el procesamiento de la solicitud. Extrae un token de los encabezados de respuesta y lo inyecta en la solicitud original.

package main

import (
  "errors"
  "net/http"
  "strings"
  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
  "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(
    "http-call",
    wrapper.ParseConfigBy(parseConfig),
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
  )
}

type MyConfig struct {
  // HTTP client for external calls
  client      wrapper.HttpClient
  // Path to request on the external service
  requestPath string
  // Response header key to extract and inject into the original request
  tokenHeader string
}

func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
  config.tokenHeader = json.Get("tokenHeader").String()
  if config.tokenHeader == "" {
    return errors.New("missing tokenHeader in config")
  }
  config.requestPath = json.Get("requestPath").String()
  if config.requestPath == "" {
    return errors.New("missing requestPath in config")
  }
  // Full FQDN with service type suffix.
  // Examples: my-svc.dns, my-svc.static,
  //   service-provider.DEFAULT-GROUP.public.nacos,
  //   httpbin.my-ns.svc.cluster.local
  serviceName := json.Get("serviceName").String()
  servicePort := json.Get("servicePort").Int()
  if servicePort == 0 {
    if strings.HasSuffix(serviceName, ".static") {
      // Default port for static IP services
      servicePort = 80
    }
  }
  config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
    FQDN: serviceName,
    Port: servicePort,
        })
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  // Send an async HTTP GET request. Default timeout is 500 ms.
  err := config.client.Get(config.requestPath, nil,
           // Callback runs when the response arrives
           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
             }
             log.Infof("get status: %d, response body: %s", statusCode, responseBody)
             // Extract the token from the response and add it to the original request
             token := responseHeaders.Get(config.tokenHeader)
             if token != "" {
               proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
             }
             // Resume the paused request so it can be forwarded to the backend
             proxywasm.ResumeHttpRequest()
    })

  if err != nil {
    // If the service call fails, let the request proceed and log the error
    log.Errorf("Error occured while calling http, it seems cannot find the service cluster.")
    return types.ActionContinue
  } else {
    // Pause the request until the async callback completes
    return types.HeaderStopAllIterationAndWatermark
  }
}

Llamada a Redis desde un plugin

El siguiente ejemplo implementa un plugin de limitación de tasa respaldado por Redis. Rastrea las solicitudes por minuto (QPM) y devuelve HTTP 429 cuando se supera el límite.

package main

import (
  "strconv"
  "time"

  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
  "github.com/tidwall/gjson"
  "github.com/tidwall/resp"

  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
)

func main() {}

func init() {
  wrapper.SetCtx(
    "redis-demo",
    wrapper.ParseConfigBy(parseConfig),
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
    wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
  )
}

type RedisCallConfig struct {
  client wrapper.RedisClient
  qpm    int
}

func parseConfig(json gjson.Result, config *RedisCallConfig, log logs.Log) error {
  // Full FQDN with service type suffix.
  // Examples: my-redis.dns, redis.my-ns.svc.cluster.local
  serviceName := json.Get("serviceName").String()
  servicePort := json.Get("servicePort").Int()
  if servicePort == 0 {
    if strings.HasSuffix(serviceName, ".static") {
      servicePort = 80
    } else {
      servicePort = 6379
    }
  }
  username := json.Get("username").String()
  password := json.Get("password").String()
  // Timeout in milliseconds
  timeout := json.Get("timeout").Int()
  if timeout == 0 {
    timeout = 1000
  }
  qpm := json.Get("qpm").Int()
  config.qpm = int(qpm)
  config.client = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
    FQDN: serviceName,
    Port: servicePort,
  })
  return config.client.Init(username, password, timeout)
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.Log) types.Action {
  now := time.Now()
  minuteAligned := now.Truncate(time.Minute)
  timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
  // If err != nil, the gateway likely cannot reach the Redis backend.
  // Verify that the Redis service has not been deleted.
  err := config.client.Incr(timeStamp, func(response resp.Value) {
    if response.Error() != nil {
      log.Errorf("call redis error: %v", response.Error())
      proxywasm.ResumeHttpRequest()
    } else {
      ctx.SetContext("timeStamp", timeStamp)
      ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm-response.Integer()))
      if response.Integer() == 1 {
        err := config.client.Expire(timeStamp, 60, func(response resp.Value) {
          if response.Error() != nil {
            log.Errorf("call redis error: %v", response.Error())
          }
          proxywasm.ResumeHttpRequest()
        })
        if err != nil {
          log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
          proxywasm.ResumeHttpRequest()
        }
      } else {
        if response.Integer() > config.qpm {
          proxywasm.SendHttpResponse(429, [][2]string{{"timeStamp", timeStamp}, {"callTimeLeft", "0"}}, []byte("Too many requests\n"), -1)
        } else {
          proxywasm.ResumeHttpRequest()
        }
      }
    }
  })
  if err != nil {
    // If the Redis call fails, let the request proceed and log the error
    log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
    return types.HeaderContinue
  } else {
    // Pause the request until the Redis callback completes
    return types.HeaderStopAllIterationAndWatermark
  }
}

func onHttpResponseHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.Log) types.Action {
  if ctx.GetContext("timeStamp") != nil {
    proxywasm.AddHttpResponseHeader("timeStamp", ctx.GetContext("timeStamp").(string))
  }
  if ctx.GetContext("callTimeLeft") != nil {
    proxywasm.AddHttpResponseHeader("callTimeLeft", ctx.GetContext("callTimeLeft").(string))
  }
  return types.HeaderContinue
}