Notice:
This is the "latest" release of Envoy Gateway, which contains the most recent commits from the main branch.
This release might not be stable.
Please refer to the /docs documentation for the most current information.

Response Override

Response Override allows you to override the response from the backend with a custom one. This can be useful for scenarios such as returning a custom 404 page when the requested resource is not found, a custom 500 error message when the backend is failing, or redirecting the client when the backend returns a 403 Forbidden. When using redirect, Envoy applies it internally: Envoy follows the redirect to the new URL, obtains the response from that URL, and sends that response to the client.

Each rule accepts an optional source field that controls which responses the rule applies to:

  • All (default) — overrides both Envoy-generated responses (e.g. auth failures, rate-limit rejections) and upstream responses with the matched status code.
  • Backend — overrides only responses from the upstream backend.
  • Local — overrides only responses generated by Envoy itself (e.g. a 401 from JWT authentication, a 429 from rate limiting). This is useful when you want to customise error messages from Envoy policies without affecting legitimate upstream responses that happen to share the same status code.

Installation

Follow the steps from the Quickstart to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP.

Prerequisites

Follow the steps below to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP.

Expand for instructions
  1. Install the Gateway API CRDs and Envoy Gateway using Helm:

    helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace
    
  2. Install the GatewayClass, Gateway, HTTPRoute and example app:

    kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml -n default
    
  3. Verify Connectivity:

    Get the External IP of the Gateway:

    export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}')
       

    Curl the example app through Envoy proxy:

    curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get
       

    The above command should succeed with status code 200.

    Get the name of the Envoy service created the by the example Gateway:

    export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
       

    Get the deployment of the Envoy service created the by the example Gateway:

    export ENVOY_DEPLOYMENT=$(kubectl get deploy -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
       

    Port forward to the Envoy service:

    kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 &
       

    Curl the example app through Envoy proxy:

    curl --verbose --header "Host: www.example.com" http://localhost:8888/get
       

    The above command should succeed with status code 200.

Testing Response Override

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: response-override
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: backend
  responseOverride:
    - match:
        statusCodes:
          - type: Value
            value: 404
      response:
        contentType: text/plain
        body:
          type: Inline
          inline: "Oops! Your request is not found."
    - match:
        statusCodes:
          - type: Value
            value: 403 # status from backend when envoy will execute the redirect
      redirect:
        statusCode: 302 # status envoy should respond to client
        scheme: https
        hostname: www.example.com
        path:
          type: ReplaceFullPath
          replaceFullPath: "/get"
    - match:
        statusCodes:
          - type: Value
            value: 500
          - type: Range
            range:
              start: 501
              end: 511
      response:
        contentType: application/json
        body:
          type: ValueRef
          valueRef:
            group: ""
            kind: ConfigMap
            name: response-override-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: response-override-config
data:
  response.body: '{"error": "Internal Server Error"}'
EOF

Save and apply the following resource to your cluster:

---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: response-override
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: backend
  responseOverride:
    - match:
        statusCodes:
          - type: Value
            value: 404
      response:
        contentType: text/plain
        body:
          type: Inline
          inline: "Oops! Your request is not found."
    - match:
        statusCodes:
          - type: Value
            value: 403 # status from backend when envoy will execute the redirect
      redirect:
        statusCode: 302 # status envoy should respond to client
        scheme: https
        hostname: www.example.com
        path:
          type: ReplaceFullPath
          replaceFullPath: "/get"
    - match:
        statusCodes:
          - type: Value
            value: 500
          - type: Range
            range:
              start: 501
              end: 511
      response:
        contentType: application/json
        body:
          type: ValueRef
          valueRef:
            group: ""
            kind: ConfigMap
            name: response-override-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: response-override-config
data:
  response.body: '{"error": "Internal Server Error"}'
curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/status/404
*   Trying 127.0.0.1:80...
* Connected to 172.18.0.200 (172.18.0.200) port 80
> GET /status/404 HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< content-type: text/plain
< content-length: 32
< date: Thu, 07 Nov 2024 09:22:29 GMT
<
* Connection #0 to host 172.18.0.200 left intact
Oops! Your request is not found.

For 403, the policy redirects to https://www.example.com/get. Envoy follows the redirect internally and sends that response to the client:

curl -L -v --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/status/403
* Host localhost:5000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5000...
* Connected to localhost (::1) port 5000
> GET /status/403 HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 302 Found
< content-type: application/json
< x-content-type-options: nosniff
< date: Thu, 26 Feb 2026 14:36:01 GMT
< content-length: 467
<
{
 "path": "/get",
 "host": "www.example.com",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.7.1"
  ],
  "X-Envoy-External-Address": [
   "127.0.0.1"
  ],
  "X-Forwarded-For": [
   "10.244.2.2"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "d69f627e-c454-46b2-86e1-25d4c18b68e4"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "backend-869c8646c5-xfm84"
* Connection #0 to host localhost left intact
}

You receive the response body from the redirect target. Then verify the 500 override:

curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/status/500
*   Trying 127.0.0.1:80...
* Connected to 172.18.0.200 (172.18.0.200) port 80
> GET /status/500 HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< content-type: application/json
< content-length: 34
< date: Thu, 07 Nov 2024 09:23:02 GMT
<
* Connection #0 to host 172.18.0.200 left intact
{"error": "Internal Server Error"}

Override Only Envoy-Generated Responses

By default, a responseOverride rule matches any response with the given status code, whether it came from the upstream backend or was generated by Envoy itself (e.g. a 401 produced by JWT authentication). Use source: Local to restrict a rule to Envoy-generated responses only, leaving legitimate upstream responses with the same code untouched.

The following example uses the foo HTTPRoute and jwt-example SecurityPolicy from the JWT Authentication task. Follow that task first, then apply the BackendTrafficPolicy below to replace Envoy’s default 401 rejection with a structured JSON error.

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: response-override-local
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
  responseOverride:
    - match:
        statusCodes:
          - type: Value
            value: 401
      source: Local
      response:
        contentType: application/json
        body:
          type: Inline
          inline: '{"error": "Authentication required. Please provide a valid token."}'
EOF

Save and apply the following resource to your cluster:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: response-override-local
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
  responseOverride:
    - match:
        statusCodes:
          - type: Value
            value: 401
      source: Local
      response:
        contentType: application/json
        body:
          type: Inline
          inline: '{"error": "Authentication required. Please provide a valid token."}'

Verify that a request to /foo without a JWT receives the custom JSON body instead of Envoy’s default plain-text 401:

curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/foo
< HTTP/1.1 401 Unauthorized
< content-type: application/json
< content-length: 67
<
{"error": "Authentication required. Please provide a valid token."}

Now verify that a backend-generated 401 is not overridden. The backend HTTPRoute from the Prerequisites section routes all paths to the backend, which echoes back whichever status code is in the URL. Sending a request to /status/401 produces a 401 that originates from the upstream, not from Envoy, so the source: Local rule on the foo route leaves it untouched:

curl --verbose --header "Host: www.example.com" http://localhost:8888/status/401
< HTTP/1.1 401 Unauthorized
< content-type: text/plain
< content-length: 0
<

The body is empty and the content-type is the backend’s own — no custom JSON, confirming that source: Local only intercepts Envoy-generated responses.

To see the inverse behaviour, change source to Backend. Now the same custom JSON body is applied to upstream 401s, but Envoy-generated 401s (e.g. from JWT authentication) pass through unchanged.

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: response-override-local
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
  responseOverride:
    - match:
        statusCodes:
          - type: Value
            value: 401
      source: Backend
      response:
        contentType: application/json
        body:
          type: Inline
          inline: '{"error": "Authentication required. Please provide a valid token."}'
EOF

Save and apply the following resource to your cluster:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: response-override-local
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
  responseOverride:
    - match:
        statusCodes:
          - type: Value
            value: 401
      source: Backend
      response:
        contentType: application/json
        body:
          type: Inline
          inline: '{"error": "Authentication required. Please provide a valid token."}'

A backend-generated 401 (via /status/401) is now overridden with the custom JSON body:

curl --verbose --header "Host: www.example.com" http://localhost:8888/status/401
< HTTP/1.1 401 Unauthorized
< content-type: application/json
< content-length: 67
<
{"error": "Authentication required. Please provide a valid token."}

However, a request to /foo without a JWT still receives Envoy’s own plain-text rejection, because source: Backend does not intercept Envoy-generated responses:

curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/foo
< HTTP/1.1 401 Unauthorized
< content-type: text/plain
< content-length: 15
<
Jwt is missing

The body is Envoy’s default message and the content-type is text/plain — no custom JSON, confirming that source: Backend only intercepts responses from the upstream backend.