All Products
Search
Document Center

Alibaba Cloud Service Mesh:Customize request and response handling with the Envoy ext_proc filter

Last Updated:Mar 11, 2026

The Envoy External Processing (ext_proc) filter routes HTTP request and response processing to an external gRPC service. Instead of writing a Wasm plug-in or embedding logic inside Envoy, you run a standalone gRPC server that inspects and modifies headers, bodies, and trailers as traffic flows through the mesh.

ext_proc is a good fit when you need to:

  • Add, remove, or rewrite HTTP headers based on business logic

  • Implement custom authentication or authorization checks outside the sidecar

  • Enrich requests with data from external systems before they reach the upstream service

  • Use a language or framework that Wasm does not support

How it works

The ext_proc filter intercepts traffic at the sidecar and forwards it to your external gRPC service for processing. The filter supports six independent processing steps, each controlled by a processing mode:

StepDirectionDescription
Request headersInboundInspect or modify headers before the request reaches the upstream
Request bodyInboundInspect or modify the request body
Request trailersInboundInspect or modify HTTP/2 request trailers
Response headersOutboundInspect or modify headers before the response reaches the downstream
Response bodyOutboundInspect or modify the response body
Response trailersOutboundInspect or modify HTTP/2 response trailers

By default, none of these steps are sent to the external processor. You explicitly enable each step by setting its processing mode to SEND in the EnvoyFilter configuration.

The following diagram illustrates the full request-response flow:

Envoy external processing flow
  1. A downstream service sends a request. Envoy intercepts it and forwards the request to the external processing service.

  2. The external processing service inspects or modifies the request, then returns the result to Envoy.

  3. Envoy applies the modifications and forwards the request to the upstream service.

  4. The upstream service returns a response. Envoy intercepts it and forwards the response to the external processing service.

  5. The external processing service inspects or modifies the response, then returns the result to Envoy.

  6. Envoy applies the modifications and forwards the response to the downstream service.

Prerequisites

Before you begin, make sure that you have:

Step 1: Write the external processing service

Build a gRPC server that implements the Envoy ExternalProcessor service interface. The server receives a stream of ProcessingRequest messages from Envoy and must return a matching ProcessingResponse for each.

The following Go snippet shows the core logic. For the complete runnable project, see ext-proc-demo on GitHub. For the full API specification, see the Envoy ext_proc proto reference.

View the core processing logic (Go)

func NewServer() *Server {
	return &Server{}
}

// Server implements the Envoy external processing server interface
// See https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ext_proc/v3/external_processor.proto
type Server struct{}

func (s *Server) Process(srv extProcPb.ExternalProcessor_ProcessServer) error {
	klog.Infof("Processing")
	ctx := srv.Context()
	for {
		select {
		case <-ctx.Done():
			klog.Infof("context done")
			return ctx.Err()
		default:
		}

		req, err := srv.Recv()
		if err == io.EOF {
			// envoy has closed the stream. Don't return anything and close this stream entirely
			return nil
		}
		if err != nil {
			return status.Errorf(codes.Unknown, "cannot receive stream request: %v", err)
		}

		resp := &extProcPb.ProcessingResponse{}
		switch v := req.Request.(type) {
        // Requests that require processing of request headers
		case *extProcPb.ProcessingRequest_RequestHeaders:
			klog.Infof("Got RequestHeaders")
			h := req.Request.(*extProcPb.ProcessingRequest_RequestHeaders)
			resp = handleRequestHeaders(h)
        // Requests that require processing of response headers
        case *extProcPb.ProcessingRequest_ResponseHeaders:
			klog.Infof("Got ResponseHeaders")
			h := req.Request.(*extProcPb.ProcessingRequest_ResponseHeaders)
			resp = handleResponseHeaders(h)
        // Requests that require processing of request headers, not currently implemented
		case *extProcPb.ProcessingRequest_RequestBody:
			klog.Infof("Got RequestBody (not currently handled)")
        // Requests that require processing of request trailers, not currently implemented
		case *extProcPb.ProcessingRequest_RequestTrailers:
			klog.Infof("Got RequestTrailers (not currently handled)")
        // Requests that require processing of response body, not currently implemented
		case *extProcPb.ProcessingRequest_ResponseBody:
			klog.Infof("Got ResponseBody (not currently handled)")
        // Requests that require processing of response trailers, not currently implemented
		case *extProcPb.ProcessingRequest_ResponseTrailers:
			klog.Infof("Got ResponseTrailers (not currently handled)")

		default:
			klog.Infof("Unknown Request type %v", v)
		}
        // Return the processing required for the request
		klog.Infof("Sending ProcessingResponse: %+v", resp)
		if err := srv.Send(resp); err != nil {
			klog.Infof("send error %v", err)
			return err
		}
	}
}

// Add x-ext-proc-header=hello-to-asm to request headers
func handleRequestHeaders(req *extProcPb.ProcessingRequest_RequestHeaders) *extProcPb.ProcessingResponse {
	klog.Infof("handle request headers: %+v\n", req)

	resp := &extProcPb.ProcessingResponse{
		Response: &extProcPb.ProcessingResponse_RequestHeaders{
			RequestHeaders: &extProcPb.HeadersResponse{
				Response: &extProcPb.CommonResponse{
					HeaderMutation: &extProcPb.HeaderMutation{
						SetHeaders: []*configPb.HeaderValueOption{
							{
								Header: &configPb.HeaderValue{
									Key:      "x-ext-proc-header",
									RawValue: []byte("hello-to-asm"),
								},
							},
						},
					},
				},
			},
		},
	}

	return resp
}

// Add x-ext-proc-header=hello-from-asm to response headers
func handleResponseHeaders(req *extProcPb.ProcessingRequest_ResponseHeaders) *extProcPb.ProcessingResponse {
	klog.Infof("handle response headers: %+v\n", req)

	resp := &extProcPb.ProcessingResponse{
		Response: &extProcPb.ProcessingResponse_ResponseHeaders{
			ResponseHeaders: &extProcPb.HeadersResponse{
				Response: &extProcPb.CommonResponse{
					HeaderMutation: &extProcPb.HeaderMutation{
						SetHeaders: []*configPb.HeaderValueOption{
							{
								Header: &configPb.HeaderValue{
									Key:      "x-ext-proc-header",
									RawValue: []byte("hello-from-asm"),
								},
							},
						},
					},
				},
			},
		},
	}

	return resp
}

This example handles two processing steps:

  • Request headers: Adds x-ext-proc-header: hello-to-asm to every inbound request.

  • Response headers: Adds x-ext-proc-header: hello-from-asm to every outbound response.

Note

Package your gRPC server as a container image using a Dockerfile and push it to a container registry before deployment.

Step 2: Deploy the external processing service

This step uses the sample ext_proc image provided by ASM. The sample service adds the header x-ext-proc-header: hello-to-asm to requests and x-ext-proc-header: hello-from-asm to responses.

  1. Create a file named ext.yaml with the following content:

        apiVersion: v1
        kind: Service
        metadata:
          name: ext-proc
          labels:
            app: ext-proc
            service: ext-proc
        spec:
          ports:
          # gRPC listener port for the external processing service
          - name: grpc
            port: 9002
            targetPort: 9002
          selector:
            app: ext-proc
        ---
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: ext-proc
        spec:
          replicas: 1
          selector:
            matchLabels:
              app: ext-proc
              version: v1
          template:
            metadata:
              labels:
                app: ext-proc
                version: v1
            spec:
              containers:
              - image: registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/ext-proc:v0.2
                imagePullPolicy: IfNotPresent
                name: ext-proc
                ports:
                - containerPort: 9002
  2. Apply the manifest and verify that the service is running:

        kubectl apply -f ext.yaml
  3. Check the pod logs to confirm that the gRPC server started: Expected output: This log confirms that the external processing service is running and listening on port 9002.

        kubectl logs deploy/ext-proc
        I1126 06:41:25.467033       1 main.go:52] Starting gRPC server on port :9002

Step 3: Configure the EnvoyFilter

Create an EnvoyFilter to insert the ext_proc filter into the sidecar's HTTP filter chain. This routes request and response headers from the HTTPBin sidecar to your external processing service.

  1. Log on to the ASM console. In the left-side navigation pane, choose Service Mesh > Mesh Management.

  2. On the Mesh Management page, click the name of the ASM instance. In the left-side navigation pane, choose Plugin Extension Center > EnvoyFilter Template.

  3. Create an EnvoyFilter named httpbin-ext-proc with the following content:

    Key fields:

    FieldDescription
    context: SIDECAR_INBOUNDApplies the filter to inbound traffic on the sidecar proxy
    portNumber: 80Targets the HTTP listener on port 80
    operation: INSERT_BEFOREInserts the ext_proc filter before the router filter in the chain
    cluster_nameThe Envoy cluster endpoint for the ext-proc service, in `outbound` format
    request_header_mode: SENDSends request headers to the external processor. The default is SKIP, which means headers are not sent
    response_header_mode: SENDSends response headers to the external processor. The default is SKIP, which means headers are not sent
        apiVersion: networking.istio.io/v1alpha3
        kind: EnvoyFilter
        spec:
          configPatches:
            - applyTo: HTTP_FILTER
              match:
                context: SIDECAR_INBOUND
                listener:
                  portNumber: 80
                  filterChain:
                    filter:
                      name: envoy.filters.network.http_connection_manager
                proxy:
                  proxyVersion: ^MIN_VERSION-MAX_VERSION.*
              patch:
                operation: INSERT_BEFORE
                value:
                  name: envoy.filters.http.ext_proc
                  typed_config:
                    '@type': >-
                      type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
                    grpc_service:
                      envoy_grpc:
                        cluster_name: outbound|9002||ext-proc.default.svc.cluster.local
                        authority: ext-proc.default.svc.cluster.local
                    processing_mode:
                      request_header_mode: SEND
                      response_header_mode: SEND

Verify the setup

Send a request through the mesh and confirm that the external processor added the expected headers.

From the sleep pod, run:

kubectl exec -it deploy/sleep -- curl httpbin:8000/headers -i

Look for these two headers in the output:

  • Response header x-ext-proc-header: hello-from-asm -- added by the external processor before the response reached the downstream

  • Request header X-Ext-Proc-Header: hello-to-asm (visible in the JSON body) -- added by the external processor before the request reached the upstream

Expected output:

HTTP/1.1 200 OK
server: envoy
date: Wed, 11 Dec 2024 06:47:59 GMT
content-type: application/json
content-length: 564
access-control-allow-origin: *
access-control-allow-credentials: true
x-envoy-upstream-service-time: 3
x-ext-proc-header: hello-from-asm

{
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin:8000",
    "User-Agent": "curl/8.1.2",
    "X-B3-Parentspanid": "5c6dd2cc9312d6bb",
    "X-B3-Sampled": "1",
    "X-B3-Spanid": "1153a2737cee4434",
    "X-B3-Traceid": "baba86b696edc75a5c6dd2cc9312d6bb",
    "X-Envoy-Attempt-Count": "1",
    "X-Ext-Proc-Header": "hello-to-asm",
    "X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/default/sa/httpbin;Hash=69d8f267c3c00b4396a83e12d14520acc9dadb1492d660e10f77e94dcad7cb06;Subject=\"\";URI=spiffe://cluster.local/ns/default/sa/sleep"
  }
}

Both headers are present, confirming that the external processing service correctly intercepts and modifies traffic through the Envoy sidecar.