Repository: envoyproxy/gateway

Issue:
envoy report warn messages
```
[2025-05-09 16:38:40.130][1][warning][misc] [source/common/protobuf/message_validator_impl.cc:39] message 'envoy.extensions.http.injected_credentials.generic.v3.Generic' is contained in proto file 'envoy/extensions/http/injected_credentials/generic/v3/generic.proto' marked as work-in-progress. API features marked as work-in-progress are not considered stable, are not covered by the threat model, are not supported by the security team, and are subject to breaking changes. Do not use this feature without understanding each of the previous points.
[2025-05-09 16:38:40.131][1][warning][misc] [source/common/protobuf/message_validator_impl.cc:39] message 'envoy.extensions.filters.http.credential_injector.v3.CredentialInjector' is contained in proto file 'envoy/extensions/filters/http/credential_injector/v3/credential_injector.proto' marked as work-in-progress. API features marked as work-in-progress are not considered stable, are not covered by the threat model, are not supported by the security team, and are subject to breaking changes. Do not use this feature without understanding each of the previous points.
[2025-05-09 16:38:40.175][1][warning][config] [source/extensions/formatter/metadata/config.cc:15] 'METADATA' formatter is treated as a built-in formatter and does not require configuration.
[2025-05-09 16:38:40.177][1][warning][config] [source/extensions/formatter/metadata/config.cc:15] 'METADATA' formatter is treated as a built-in formatter and does not require configuration.
```

Hints from issue comments:
@zhaohuabing: Is this also an issue for us?

2025-05-09 16:38:40.177][1][warning][config] [source/extensions/formatter/metadata/config.cc:15] 'METADATA' formatter is treated as a built-in formatter and does not require configuration.@zirain: It should be fixed in https://github.com/envoyproxy/gateway/pull/5985

Solution patch:
diff --git a/internal/xds/translator/accesslog.go b/internal/xds/translator/accesslog.go
index e707c24da..ef9ffca1e 100644
--- a/internal/xds/translator/accesslog.go
+++ b/internal/xds/translator/accesslog.go
@@ -16,8 +16,6 @@ import (
 	cel "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/filters/cel/v3"
 	grpcaccesslog "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
 	otelaccesslog "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/open_telemetry/v3"
-	celformatter "github.com/envoyproxy/go-control-plane/envoy/extensions/formatter/cel/v3"
-	metadataformatter "github.com/envoyproxy/go-control-plane/envoy/extensions/formatter/metadata/v3"
 	reqwithoutqueryformatter "github.com/envoyproxy/go-control-plane/envoy/extensions/formatter/req_without_query/v3"
 	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
 	otlpcommonv1 "go.opentelemetry.io/proto/otlp/common/v1"
@@ -35,8 +33,6 @@ const (
 	otelAccessLog = "envoy.access_loggers.open_telemetry"
 
 	reqWithoutQueryCommandOperator = "%REQ_WITHOUT_QUERY"
-	metadataCommandOperator        = "%METADATA"
-	celCommandOperator             = "%CEL"
 
 	tcpGRPCAccessLog = "envoy.access_loggers.tcp_grpc"
 	celFilter        = "envoy.access_loggers.extension_filters.cel"
@@ -76,45 +72,17 @@ var listenerAccessLogFilter = &accesslog.AccessLogFilter{
 	},
 }
 
-var (
-	// reqWithoutQueryFormatter configures additional formatters needed for some of the format strings like "REQ_WITHOUT_QUERY"
-	reqWithoutQueryFormatter *cfgcore.TypedExtensionConfig
-
-	// metadataFormatter configures additional formatters needed for some of the format strings like "METADATA"
-	// for more information, see https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/formatter/metadata/v3/metadata.proto
-	metadataFormatter *cfgcore.TypedExtensionConfig
-
-	// celFormatter configures additional formatters needed for some of the format strings like "CEL"
-	// for more information, see https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/formatter/cel/v3/cel.proto
-	celFormatter *cfgcore.TypedExtensionConfig
-)
+// reqWithoutQueryFormatter configures additional formatters needed for some of the format strings like "REQ_WITHOUT_QUERY"
+var reqWithoutQueryFormatter *cfgcore.TypedExtensionConfig
 
 func init() {
-	any, err := proto.ToAnyWithValidation(&reqwithoutqueryformatter.ReqWithoutQuery{})
+	anyCfg, err := proto.ToAnyWithValidation(&reqwithoutqueryformatter.ReqWithoutQuery{})
 	if err != nil {
 		panic(err)
 	}
 	reqWithoutQueryFormatter = &cfgcore.TypedExtensionConfig{
 		Name:        "envoy.formatter.req_without_query",
-		TypedConfig: any,
-	}
-
-	any, err = proto.ToAnyWithValidation(&metadataformatter.Metadata{})
-	if err != nil {
-		panic(err)
-	}
-	metadataFormatter = &cfgcore.TypedExtensionConfig{
-		Name:        "envoy.formatter.metadata",
-		TypedConfig: any,
-	}
-
-	any, err = proto.ToAnyWithValidation(&celformatter.Cel{})
-	if err != nil {
-		panic(err)
-	}
-	celFormatter = &cfgcore.TypedExtensionConfig{
-		Name:        "envoy.formatter.cel",
-		TypedConfig: any,
+		TypedConfig: anyCfg,
 	}
 }
 
@@ -388,7 +356,7 @@ func celAccessLogFilter(expr string) (*accesslog.AccessLogFilter, error) {
 	fl := &cel.ExpressionFilter{
 		Expression: expr,
 	}
-	any, err := proto.ToAnyWithValidation(fl)
+	anyCfg, err := proto.ToAnyWithValidation(fl)
 	if err != nil {
 		return nil, err
 	}
@@ -397,7 +365,7 @@ func celAccessLogFilter(expr string) (*accesslog.AccessLogFilter, error) {
 		FilterSpecifier: &accesslog.AccessLogFilter_ExtensionFilter{
 			ExtensionFilter: &accesslog.ExtensionFilter{
 				Name:       celFilter,
-				ConfigType: &accesslog.ExtensionFilter_TypedConfig{TypedConfig: any},
+				ConfigType: &accesslog.ExtensionFilter_TypedConfig{TypedConfig: anyCfg},
 			},
 		},
 	}, nil
@@ -441,36 +409,20 @@ func accessLogTextFormatters(text string) []*cfgcore.TypedExtensionConfig {
 		formatters = append(formatters, reqWithoutQueryFormatter)
 	}
 
-	if strings.Contains(text, metadataCommandOperator) {
-		formatters = append(formatters, metadataFormatter)
-	}
-
-	if strings.Contains(text, celCommandOperator) {
-		formatters = append(formatters, celFormatter)
-	}
-
 	return formatters
 }
 
 func accessLogJSONFormatters(json map[string]string) []*cfgcore.TypedExtensionConfig {
-	reqWithoutQuery, metadata, cel := false, false, false
+	reqWithoutQuery := false
 
 	for _, value := range json {
-		if reqWithoutQuery && metadata && cel {
+		if reqWithoutQuery {
 			break
 		}
 
 		if strings.Contains(value, reqWithoutQueryCommandOperator) {
 			reqWithoutQuery = true
 		}
-
-		if strings.Contains(value, metadataCommandOperator) {
-			metadata = true
-		}
-
-		if strings.Contains(value, celCommandOperator) {
-			cel = true
-		}
 	}
 
 	formatters := make([]*cfgcore.TypedExtensionConfig, 0, 3)
@@ -479,64 +431,28 @@ func accessLogJSONFormatters(json map[string]string) []*cfgcore.TypedExtensionCo
 		formatters = append(formatters, reqWithoutQueryFormatter)
 	}
 
-	if metadata {
-		formatters = append(formatters, metadataFormatter)
-	}
-
-	if cel {
-		formatters = append(formatters, celFormatter)
-	}
-
 	return formatters
 }
 
 func accessLogOpenTelemetryFormatters(body string, attributes map[string]string) []*cfgcore.TypedExtensionConfig {
-	reqWithoutQuery, metadata, cel := false, false, false
+	var reqWithoutQuery bool
 
 	if strings.Contains(body, reqWithoutQueryCommandOperator) {
 		reqWithoutQuery = true
 	}
 
-	if strings.Contains(body, metadataCommandOperator) {
-		metadata = true
-	}
-
-	if strings.Contains(body, celCommandOperator) {
-		cel = true
-	}
-
 	for _, value := range attributes {
-		if reqWithoutQuery && metadata && cel {
-			break
-		}
-
-		if !reqWithoutQuery && strings.Contains(value, reqWithoutQueryCommandOperator) {
+		if strings.Contains(value, reqWithoutQueryCommandOperator) {
 			reqWithoutQuery = true
-		}
-
-		if !metadata && strings.Contains(value, metadataCommandOperator) {
-			metadata = true
-		}
-
-		if !cel && strings.Contains(value, celCommandOperator) {
-			cel = true
+			break
 		}
 	}
 
 	formatters := make([]*cfgcore.TypedExtensionConfig, 0, 3)
-
 	if reqWithoutQuery {
 		formatters = append(formatters, reqWithoutQueryFormatter)
 	}
 
-	if metadata {
-		formatters = append(formatters, metadataFormatter)
-	}
-
-	if cel {
-		formatters = append(formatters, celFormatter)
-	}
-
 	return formatters
 }
 



Test patch:
diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.listeners.yaml
index 6efcf6de1..7e51cbeb1 100644
--- a/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.listeners.yaml
@@ -7,10 +7,6 @@
     typedConfig:
       '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
       logFormat:
-        formatters:
-        - name: envoy.formatter.cel
-          typedConfig:
-            '@type': type.googleapis.com/envoy.extensions.formatter.cel.v3.Cel
         textFormatSource:
           inlineString: |
             [%START_TIME%] "%CEL(request.method)% %CEL(request.path)% %CEL(request.protocol)%" %CEL(response.code)% %CEL(response.flags)% %CEL(request.total_size)% %CEL(response.total_size)% %CEL(request.duration)% %CEL(response.headers['X-ENVOY-UPSTREAM-SERVICE-TIME'])% "%CEL(request.headers['X-FORWARDED-FOR'])%" "%CEL(request.useragent)%" "%CEL(request.id)%" "%CEL(request.host)%" "%CEL(upstream.address)%"
@@ -23,10 +19,6 @@
     typedConfig:
       '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
       logFormat:
-        formatters:
-        - name: envoy.formatter.metadata
-          typedConfig:
-            '@type': type.googleapis.com/envoy.extensions.formatter.metadata.v3.Metadata
         jsonFormat:
           method: '%REQ(:METHOD)%'
           path: '%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%'
@@ -92,12 +84,6 @@
       - name: envoy.formatter.req_without_query
         typedConfig:
           '@type': type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery
-      - name: envoy.formatter.metadata
-        typedConfig:
-          '@type': type.googleapis.com/envoy.extensions.formatter.metadata.v3.Metadata
-      - name: envoy.formatter.cel
-        typedConfig:
-          '@type': type.googleapis.com/envoy.extensions.formatter.cel.v3.Cel
       resourceAttributes:
         values:
         - key: cluster_name
@@ -117,10 +103,6 @@
           typedConfig:
             '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
             logFormat:
-              formatters:
-              - name: envoy.formatter.cel
-                typedConfig:
-                  '@type': type.googleapis.com/envoy.extensions.formatter.cel.v3.Cel
               textFormatSource:
                 inlineString: |
                   [%START_TIME%] "%CEL(request.method)% %CEL(request.path)% %CEL(request.protocol)%" %CEL(response.code)% %CEL(response.flags)% %CEL(request.total_size)% %CEL(response.total_size)% %CEL(request.duration)% %CEL(response.headers['X-ENVOY-UPSTREAM-SERVICE-TIME'])% "%CEL(request.headers['X-FORWARDED-FOR'])%" "%CEL(request.useragent)%" "%CEL(request.id)%" "%CEL(request.host)%" "%CEL(upstream.address)%"
@@ -129,10 +111,6 @@
           typedConfig:
             '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
             logFormat:
-              formatters:
-              - name: envoy.formatter.metadata
-                typedConfig:
-                  '@type': type.googleapis.com/envoy.extensions.formatter.metadata.v3.Metadata
               jsonFormat:
                 method: '%REQ(:METHOD)%'
                 path: '%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%'
@@ -190,12 +168,6 @@
             - name: envoy.formatter.req_without_query
               typedConfig:
                 '@type': type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery
-            - name: envoy.formatter.metadata
-              typedConfig:
-                '@type': type.googleapis.com/envoy.extensions.formatter.metadata.v3.Metadata
-            - name: envoy.formatter.cel
-              typedConfig:
-                '@type': type.googleapis.com/envoy.extensions.formatter.cel.v3.Cel
             resourceAttributes:
               values:
               - key: cluster_name
diff --git a/test/e2e/testdata/btp-timeout.yaml b/test/e2e/testdata/btp-timeout.yaml
new file mode 100644
index 000000000..19f0cd541
--- /dev/null
+++ b/test/e2e/testdata/btp-timeout.yaml
@@ -0,0 +1,30 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: timeout
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: http-btp-timeout
+  timeout:
+    http:
+      requestTimeout: 2s
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-btp-timeout
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /btp-timeout
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
diff --git a/test/e2e/testdata/client-timeout.yaml b/test/e2e/testdata/client-timeout.yaml
index 43cf26ec3..eea250993 100644
--- a/test/e2e/testdata/client-timeout.yaml
+++ b/test/e2e/testdata/client-timeout.yaml
@@ -38,7 +38,7 @@ spec:
     - matches:
         - path:
             type: PathPrefix
-            value: /request-timeout
+            value: /client-timeout
       backendRefs:
         - name: infra-backend-v1
           port: 8080
diff --git a/test/e2e/tests/btp_timeout.go b/test/e2e/tests/btp_timeout.go
new file mode 100644
index 000000000..0baaa72ad
--- /dev/null
+++ b/test/e2e/tests/btp_timeout.go
@@ -0,0 +1,42 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"net/http"
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, BTPTimeoutTest)
+}
+
+var BTPTimeoutTest = suite.ConformanceTest{
+	ShortName:   "BTPTimeout",
+	Description: "Test BackendTrafficPolicy timeout",
+	Manifests:   []string{"testdata/btp-timeout.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("HTTP", func(t *testing.T) {
+			routeNN := types.NamespacedName{Name: "http-btp-timeout", Namespace: ConformanceInfraNamespace}
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName,
+				SameNamespaceGatewayRef, routeNN)
+
+			ExpectRequestTimeout(t, suite, gwAddr, "/btp-timeout", "", http.StatusOK)
+			// Timeout is 2s, so deplay 1s will return 200
+			ExpectRequestTimeout(t, suite, gwAddr, "/btp-timeout", "delay=1s", http.StatusOK)
+			// Timeout is 2s, so deplay 4s will return 504
+			ExpectRequestTimeout(t, suite, gwAddr, "/btp-timeout", "delay=4s", http.StatusGatewayTimeout)
+		})
+
+		// TODO: add test for TCP
+	},
+}
diff --git a/test/e2e/tests/client_timeout.go b/test/e2e/tests/client_timeout.go
index faffec9df..e655d1398 100644
--- a/test/e2e/tests/client_timeout.go
+++ b/test/e2e/tests/client_timeout.go
@@ -9,15 +9,11 @@ package tests
 
 import (
 	"net/http"
-	"net/url"
 	"testing"
-	"time"
 
 	"k8s.io/apimachinery/pkg/types"
-	httputils "sigs.k8s.io/gateway-api/conformance/utils/http"
 	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
 	"sigs.k8s.io/gateway-api/conformance/utils/suite"
-	"sigs.k8s.io/gateway-api/conformance/utils/tlog"
 )
 
 func init() {
@@ -30,38 +26,11 @@ var ClientTimeoutTest = suite.ConformanceTest{
 	Manifests:   []string{"testdata/client-timeout.yaml"},
 	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
 		t.Run("http client timeout", func(t *testing.T) {
-			ns := "gateway-conformance-infra"
-			routeNN := types.NamespacedName{Name: "http-client-timeout", Namespace: ns}
-			gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
-			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+			routeNN := types.NamespacedName{Name: "http-client-timeout", Namespace: ConformanceInfraNamespace}
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName,
+				SameNamespaceGatewayRef, routeNN)
 
-			// Use raw http request to avoid chunked
-			req := &http.Request{
-				Method: "GET",
-				URL:    &url.URL{Scheme: "http", Host: gwAddr, Path: "/request-timeout"},
-			}
-
-			client := &http.Client{}
-
-			httputils.AwaitConvergence(t,
-				suite.TimeoutConfig.RequiredConsecutiveSuccesses,
-				suite.TimeoutConfig.MaxTimeToConsistency,
-				func(elapsed time.Duration) bool {
-					resp, err := client.Do(req)
-					if err != nil {
-						panic(err)
-					}
-					defer resp.Body.Close()
-
-					// return 504 instead of 400 when request timeout.
-					// https://github.com/envoyproxy/envoy/blob/56021dbfb10b53c6d08ed6fc811e1ff4c9ac41fd/source/common/http/utility.cc#L1409
-					if http.StatusGatewayTimeout == resp.StatusCode {
-						return true
-					} else {
-						tlog.Logf(t, "response status code: %d, (after %v) ", resp.StatusCode, elapsed)
-						return false
-					}
-				})
+			ExpectRequestTimeout(t, suite, gwAddr, "/client-timeout", "", http.StatusGatewayTimeout)
 		})
 	},
 }
diff --git a/test/e2e/tests/eg_upgrade.go b/test/e2e/tests/eg_upgrade.go
index f1d99b150..7e37aa454 100644
--- a/test/e2e/tests/eg_upgrade.go
+++ b/test/e2e/tests/eg_upgrade.go
@@ -162,6 +162,11 @@ var EGUpgradeTest = suite.ConformanceTest{
 				t.Errorf("failed to get expected response for the first three requests: %v", err)
 			}
 		})
+		t.Cleanup(func() {
+			if t.Failed() {
+				CollectAndDump(t, suite.RestConfig)
+			}
+		})
 	},
 }
 
diff --git a/test/e2e/tests/merge_gateways.go b/test/e2e/tests/merge_gateways.go
index 018856d0f..1e62f5706 100644
--- a/test/e2e/tests/merge_gateways.go
+++ b/test/e2e/tests/merge_gateways.go
@@ -246,8 +246,10 @@ var MergeGatewaysTest = suite.ConformanceTest{
 
 		// Clean-up the conflicted gateway and route resources.
 		t.Cleanup(func() {
-			// Collect and dump every config before removing the created resource.
-			CollectAndDump(t, suite.RestConfig)
+			if t.Failed() {
+				// Collect and dump every config before removing the created resource.
+				CollectAndDump(t, suite.RestConfig)
+			}
 
 			conflictedGateway := new(gwapiv1.Gateway)
 			conflictedHTTPRoute := new(gwapiv1.HTTPRoute)
diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go
index 5700f82af..1a628a215 100644
--- a/test/e2e/tests/utils.go
+++ b/test/e2e/tests/utils.go
@@ -38,6 +38,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 	"sigs.k8s.io/gateway-api/conformance/utils/config"
+	httputils "sigs.k8s.io/gateway-api/conformance/utils/http"
+	k8sutils "sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
 	"sigs.k8s.io/gateway-api/conformance/utils/suite"
 	"sigs.k8s.io/gateway-api/conformance/utils/tlog"
 
@@ -49,11 +51,18 @@ import (
 var (
 	IPFamily      = os.Getenv("IP_FAMILY")
 	DeployProfile = os.Getenv("KUBE_DEPLOY_PROFILE")
+
+	SameNamespaceGateway    = types.NamespacedName{Name: "same-namespace", Namespace: ConformanceInfraNamespace}
+	SameNamespaceGatewayRef = k8sutils.NewGatewayRef(SameNamespaceGateway)
+
+	PodReady = corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue}
 )
 
-const defaultServiceStartupTimeout = 5 * time.Minute
+const (
+	ConformanceInfraNamespace = "gateway-conformance-infra"
 
-var PodReady = corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue}
+	defaultServiceStartupTimeout = 5 * time.Minute
+)
 
 // WaitForPods waits for the pods in the given namespace and with the given selector
 // to be in the given phase and condition.
@@ -702,3 +711,34 @@ func GetGatewayResourceNamespace() string {
 	}
 	return "envoy-gateway-system"
 }
+
+func ExpectRequestTimeout(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr, path, query string, exceptedStatusCode int) {
+	// Use raw http request to avoid chunked
+	req := &http.Request{
+		Method: "GET",
+		URL:    &url.URL{Scheme: "http", Host: gwAddr, Path: path, RawQuery: query},
+	}
+
+	client := &http.Client{
+		Timeout: 10 * time.Second,
+	}
+	httputils.AwaitConvergence(t, suite.TimeoutConfig.RequiredConsecutiveSuccesses, suite.TimeoutConfig.MaxTimeToConsistency,
+		func(elapsed time.Duration) bool {
+			resp, err := client.Do(req)
+			if err != nil {
+				panic(err)
+			}
+			defer func() {
+				_ = resp.Body.Close()
+			}()
+
+			// return 504 instead of 400 when request timeout.
+			// https://github.com/envoyproxy/envoy/blob/56021dbfb10b53c6d08ed6fc811e1ff4c9ac41fd/source/common/http/utility.cc#L1409
+			if exceptedStatusCode == resp.StatusCode {
+				return true
+			} else {
+				tlog.Logf(t, "%s%s response status code: %d after %v", gwAddr, path, resp.StatusCode, elapsed)
+				return false
+			}
+		})
+}