Skip to content

Configuration and headers

We'll learn how to add additional headers to HTTP responses in this lab. We'll use the main.go file created in the previous lab.

Adding HTTP response header

We'll create the OnHttpResponseHeaders function, and within the function, we'll add a new response header using the AddHttpResponseHeader from the SDK.

Create the OnHttpResponseHeaders function that looks like this:

func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
  proxywasm.LogInfo("OnHttpResponseHeaders")
  err := proxywasm.AddHttpResponseHeader("my-new-header", "some-value-here")
  if err != nil {
    proxywasm.LogCriticalf("failed to add response header: %v", err)
  }
  return types.ActionContinue
}

Let's rebuild the extension:

tinygo build -o main.wasm -scheduler=none -target=wasi main.go

And we can now re-run the Envoy proxy with the updated extension:

func-e run -c envoy.yaml &

Now, if we send a request again (make sure to add the -v flag), we'll see the header that got added to the response:

curl -v localhost:18000
...
< HTTP/1.1 200 OK
< content-length: 13
< content-type: text/plain
< my-new-header: some-value-here
< date: Mon, 22 Jun 2021 17:02:31 GMT
< server: envoy
<
hello world

You can stop running Envoy by typing fg and pressing Ctrl+C.

Reading values from configuration

Hard-coded values in code are never a good idea. Let's see how we could read the additional headers from the configuration we provided in the Envoy configuration file.

Let's start by adding the plumbing that will allow us to read the headers from the configuration.

  1. Add the additionalHeaders and contextID to the pluginContext struct:

    type pluginContext struct {
      // Embed the default plugin context here,
      // so that we don't need to reimplement all the methods.
      types.DefaultPluginContext
      additionalHeaders map[string]string
      contextID         uint32
    }
    

Note

The additionalHeaders variables is a map of strings that will store header keys and values we'll read from the configuration.

  1. Update the NewPluginContext function to return the plugin context with initialized variables:

    func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
      return &pluginContext{contextID: contextID, additionalHeaders: map[string]string{}}
    }
    

As for reading the configuration - we have two options - we can read the configuration set at the plugin level using the GetPluginConfiguration function or at the VM level using the GetVMConfiguration function.

Typically, you'd read the configuration when the plugin starts (OnPluginStart) or when the VM starts (OnVMStart).

Parsing JSON from config file

Let's add the OnPluginStart function where we read in values from the Envoy configuration and store the key/value pairs in the additionalHeaders map. We'll use the fastjson library (github.com/valyala/fastjson) to parse the JSON string:

func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
  data, err := proxywasm.GetPluginConfiguration() // (1)
  if err != nil {
    proxywasm.LogCriticalf("error reading plugin configuration: %v", err)
  }

  var p fastjson.Parser
  v, err := p.ParseBytes(data)
  if err != nil {
    proxywasm.LogCriticalf("error parsing configuration: %v", err)
  }

  obj, err := v.Object()
  if err != nil {
    proxywasm.LogCriticalf("error getting object from json value: %v", err)
  }

  obj.Visit(func(k []byte, v *fastjson.Value) {
    ctx.additionalHeaders[string(k)] = string(v.GetStringBytes()) // (2)
  })

  return types.OnPluginStartStatusOK
}
  1. We use the GetPluginConfiguration function to read the configuration section from the Envoy config file.
  2. We iterate through all key/value pairs from the configuration and store them in the additionalHeaders map.

Note

Make sure to add the github.com/valyala/fastjson to the import statements at the top of the file and run go mod tidy to download the dependency.

To access the configuration values we've set, we need to add the map to the HTTP context when we initialize it. To do that, we need to update the httpContext struct first and add the additionalHeaders map:

type httpContext struct {
  // Embed the default http context here,
  // so that we don't need to reimplement all the methods.
  types.DefaultHttpContext
  contextID         uint32
  additionalHeaders map[string]string
}

Then, in the NewHttpContext function we can instantiate the httpContext with the additionalHeaders map coming from the pluginContext:

func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
  return &httpContext{contextID: contextID, additionalHeaders: ctx.additionalHeaders}
}

Calling AddHttpResponseHeader

Finally, in order to set the headers we modify the OnHttpResponseHeaders function, iterate through the additionalHeaders map and call the AddHttpResponseHeader for each item:

func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
  proxywasm.LogInfo("OnHttpResponseHeaders")

  for key, value := range ctx.additionalHeaders { // (1)
    if err := proxywasm.AddHttpResponseHeader(key, value); err != nil { // (2)
        proxywasm.LogCriticalf("failed to add header: %v", err)
        return types.ActionPause
    }
    proxywasm.LogInfof("header set: %s=%s", key, value) // (3)
  }

  return types.ActionContinue
}
  1. Iterate through the additionalHeaders map created in the NewPluginContext. The map contains the data from the config file we parsed in the OnPluginStart function

  2. We call the AddHttpResponseHeader to set the response headers

  3. We also log out the header key and value
Complete main.go file
main.go
package main

import (
    "github.com/valyala/fastjson"

    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

func main() {
    proxywasm.SetVMContext(&vmContext{})
}

// Override types.DefaultPluginContext.
func (ctx pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
    data, err := proxywasm.GetPluginConfiguration()
    if err != nil {
        proxywasm.LogCriticalf("error reading plugin configuration: %v", err)
    }

    var p fastjson.Parser
    v, err := p.ParseBytes(data)
    if err != nil {
        proxywasm.LogCriticalf("error parsing configuration: %v", err)
    }

    obj, err := v.Object()
    if err != nil {
        proxywasm.LogCriticalf("error getting object from json value: %v", err)
    }

    obj.Visit(func(k []byte, v *fastjson.Value) {
        ctx.additionalHeaders[string(k)] = string(v.GetStringBytes())
    })

    return types.OnPluginStartStatusOK
}

type vmContext struct {
    // Embed the default VM context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultVMContext
}

// Override types.DefaultVMContext.
func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
    return &pluginContext{contextID: contextID, additionalHeaders: map[string]string{}}
}

type pluginContext struct {
    // Embed the default plugin context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultPluginContext
    additionalHeaders map[string]string
    contextID         uint32
}

// Override types.DefaultPluginContext.
func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
    proxywasm.LogInfo("NewHttpContext")
    return &httpContext{contextID: contextID, additionalHeaders: ctx.additionalHeaders}
}

type httpContext struct {
    // Embed the default http context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultHttpContext
    contextID         uint32
    additionalHeaders map[string]string
}

func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
    proxywasm.LogInfo("OnHttpResponseHeaders")

    for key, value := range ctx.additionalHeaders {
        if err := proxywasm.AddHttpResponseHeader(key, value); err != nil {
            proxywasm.LogCriticalf("failed to add header: %v", err)
            return types.ActionPause
        }
        proxywasm.LogInfof("header set: %s=%s", key, value)
    }

    return types.ActionContinue
}

Let's rebuild the extension again:

tinygo build -o main.wasm -scheduler=none -target=wasi main.go

We also have to update the config file to include additional headers in the filter configuration (the configuration field):

- name: envoy.filters.http.wasm
  typed_config:
    '@type': type.googleapis.com/udpa.type.v1.TypedStruct
    type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
    value:
      config:
        vm_config:
          runtime: 'envoy.wasm.runtime.v8'
          code:
            local:
              filename: 'main.wasm'
        configuration:
          '@type': type.googleapis.com/google.protobuf.StringValue
          value: |
            { 
              "header_1": "somevalue", 
              "header_2": "secondvalue"
            }
Complete envoy.yaml file
envoy.yaml
static_resources:
  listeners:
    - name: main
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 18000
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                codec_type: auto
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          direct_response:
                            status: 200
                            body:
                              inline_string: "hello world\n"
                http_filters:
                  - name: envoy.filters.http.wasm
                    typed_config:
                      "@type": type.googleapis.com/udpa.type.v1.TypedStruct
                      type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                      value:
                        config:
                          configuration:
                            "@type": type.googleapis.com/google.protobuf.StringValue
                            value: |
                                { 
                                  "header_1": "some_value_1", 
                                  "header_2": "another_value"
                                }
                          vm_config:
                            vm_id: "my_vm"
                            runtime: "envoy.wasm.runtime.v8"
                            code:
                              local:
                                filename: "main.wasm"
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

We can re-run the Envoy proxy with the updated configuration using func-e run -c envoy.yaml &.

This time, when we send a request, we'll notice the headers we set in the extension configuration is added to the response:

curl -v localhost:18000
...
< HTTP/1.1 200 OK
< content-length: 13
< content-type: text/plain
< header_1: somevalue
< header_2: secondvalue
< date: Mon, 22 Jun 2021 17:54:53 GMT
< server: envoy
...

You can stop running Envoy by bringing it to the foreground using fg and pressing Ctrl+C.