From a9e59ccfb89b7b60db1bcd8284c9b07dd08215d6 Mon Sep 17 00:00:00 2001 From: shuqz Date: Thu, 20 Nov 2025 10:48:04 -0800 Subject: [PATCH 1/2] [feat gw-api]modify redirect ReplacePrefixMatch support behavior --- docs/guide/gateway/l7gateway.md | 41 ++++++++++- pkg/gateway/routeutils/route_rule_action.go | 12 +-- .../routeutils/route_rule_action_test.go | 73 +++++++++++++++---- .../routeutils/route_rule_transform.go | 7 +- test/e2e/gateway/alb_instance_target_test.go | 2 +- test/e2e/gateway/alb_ip_target_test.go | 2 +- 6 files changed, 111 insertions(+), 26 deletions(-) diff --git a/docs/guide/gateway/l7gateway.md b/docs/guide/gateway/l7gateway.md index 63c2900bd..66466f47f 100644 --- a/docs/guide/gateway/l7gateway.md +++ b/docs/guide/gateway/l7gateway.md @@ -185,7 +185,7 @@ information see the [Gateway API Conformance Page](https://gateway-api.sigs.k8s. | HTTPRouteRule - HTTPRouteFilter - RequestHeaderModifier | Core | ❌-- [Limited Support](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/header-modification.html) | | HTTPRouteRule - HTTPRouteFilter - ResponseHeaderModifier | Core | ❌ | | HTTPRouteRule - HTTPRouteFilter - RequestMirror | Extended | ❌ | -| HTTPRouteRule - HTTPRouteFilter - RequestRedirect | Core | ✅ | +| HTTPRouteRule - HTTPRouteFilter - RequestRedirect | Core | ✅ -- See [ReplacePrefixMatch Limitation](#requestredirect-path-modification-replaceprefixmatch-limitation) below | | HTTPRouteRule - HTTPRouteFilter - UrlRewrite | Extended | ✅ | | HTTPRouteRule - HTTPRouteFilter - CORS | Extended | ❌ | | HTTPRouteRule - HTTPRouteFilter - ExternalAuth | Extended | ❌ -- Use [ListenerRuleConfigurations](customization.md#customizing-l7-routing-rules) | @@ -200,8 +200,43 @@ information see the [Gateway API Conformance Page](https://gateway-api.sigs.k8s. Backend TLS is not supported by AWS ALB Gateway. For more information on how AWS ALB communicates with targets using encryption, please see the [AWS documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-routing-configuration). - - +##### RequestRedirect Path Modification ReplacePrefixMatch Limitation + +The AWS Load Balancer Controller supports HTTPRoute RequestRedirect filters with both `ReplaceFullPath` and `ReplacePrefixMatch` path modification types. + +**ReplacePrefixMatch Behavior:** + +The behavior of `ReplacePrefixMatch` depends on whether other redirect components are modified: + +1. **With scheme/port/hostname changes** - Path suffixes are preserved: + ```yaml + filters: + - type: RequestRedirect + requestRedirect: + scheme: HTTPS # or port/hostname + path: + type: ReplacePrefixMatch + replacePrefixMatch: /new-prefix + ``` + - Request: `/old-prefix/path/to/resource` + - Redirects to: `/new-prefix/path/to/resource` ✅ (suffix preserved) + +2. **Without other component changes** - Only prefix is replaced, suffixes are NOT preserved: + ```yaml + filters: + - type: RequestRedirect + requestRedirect: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /new-prefix + ``` + - Request: `/old-prefix/path/to/resource` + - Redirects to: `/new-prefix` ❌ (suffix lost) + +**Recommendations:** + +- For path-only redirects with exact paths, use `ReplaceFullPath` +- To preserve path suffixes with prefix replacement, also modify `scheme`, `port`, or `hostname` #### Examples diff --git a/pkg/gateway/routeutils/route_rule_action.go b/pkg/gateway/routeutils/route_rule_action.go index 7bdbdb641..b1c7ab7e9 100644 --- a/pkg/gateway/routeutils/route_rule_action.go +++ b/pkg/gateway/routeutils/route_rule_action.go @@ -259,12 +259,14 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon path = filter.Path.ReplaceFullPath isComponentSpecified = true } else if filter.Path.ReplacePrefixMatch != nil { - pathValue := *filter.Path.ReplacePrefixMatch - if strings.ContainsAny(pathValue, "*?") { - return nil, errors.Errorf("ReplacePrefixMatch shouldn't contain wildcards: %v", pathValue) + // Use #{path} if other components are modified (avoids redirect loop) + // Otherwise use literal prefix (no suffix preservation) + if filter.Scheme != nil || filter.Port != nil || filter.Hostname != nil { + pathVariable := "/#{path}" + path = &pathVariable + } else { + path = filter.Path.ReplacePrefixMatch } - processedPath := fmt.Sprintf("%s/*", pathValue) - path = &processedPath isComponentSpecified = true } } diff --git a/pkg/gateway/routeutils/route_rule_action_test.go b/pkg/gateway/routeutils/route_rule_action_test.go index 32a0c000a..ae730fc4f 100644 --- a/pkg/gateway/routeutils/route_rule_action_test.go +++ b/pkg/gateway/routeutils/route_rule_action_test.go @@ -2,6 +2,8 @@ package routeutils import ( "context" + "testing" + awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -13,7 +15,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" gwv1 "sigs.k8s.io/gateway-api/apis/v1" - "testing" ) func Test_buildHttpRedirectAction(t *testing.T) { @@ -27,7 +28,6 @@ func Test_buildHttpRedirectAction(t *testing.T) { query := "test-query" replaceFullPath := "/new-path" replacePrefixPath := "/new-prefix-path" - replacePrefixPathAfterProcessing := "/new-prefix-path/*" invalidPath := "/invalid-path*" tests := []struct { @@ -66,8 +66,43 @@ func Test_buildHttpRedirectAction(t *testing.T) { wantErr: false, }, { - name: "redirect with prefix match", + name: "redirect with prefix match only - uses literal prefix", + filter: &gwv1.HTTPRequestRedirectFilter{ + Path: &gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: &replacePrefixPath, + }, + }, + want: &elbv2model.Action{ + Type: elbv2model.ActionTypeRedirect, + RedirectConfig: &elbv2model.RedirectActionConfig{ + Path: &replacePrefixPath, + }, + }, + wantErr: false, + }, + { + name: "redirect with prefix match and scheme - uses #{path}", + filter: &gwv1.HTTPRequestRedirectFilter{ + Scheme: &scheme, + Path: &gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: &replacePrefixPath, + }, + }, + want: &elbv2model.Action{ + Type: elbv2model.ActionTypeRedirect, + RedirectConfig: &elbv2model.RedirectActionConfig{ + Path: awssdk.String("/#{path}"), + Protocol: &expectedScheme, + }, + }, + wantErr: false, + }, + { + name: "redirect with prefix match and port - uses #{path}", filter: &gwv1.HTTPRequestRedirectFilter{ + Port: (*gwv1.PortNumber)(&port), Path: &gwv1.HTTPPathModifier{ Type: gwv1.PrefixMatchHTTPPathModifier, ReplacePrefixMatch: &replacePrefixPath, @@ -76,7 +111,26 @@ func Test_buildHttpRedirectAction(t *testing.T) { want: &elbv2model.Action{ Type: elbv2model.ActionTypeRedirect, RedirectConfig: &elbv2model.RedirectActionConfig{ - Path: &replacePrefixPathAfterProcessing, + Path: awssdk.String("/#{path}"), + Port: &portString, + }, + }, + wantErr: false, + }, + { + name: "redirect with prefix match and hostname - uses #{path}", + filter: &gwv1.HTTPRequestRedirectFilter{ + Hostname: (*gwv1.PreciseHostname)(&hostname), + Path: &gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: &replacePrefixPath, + }, + }, + want: &elbv2model.Action{ + Type: elbv2model.ActionTypeRedirect, + RedirectConfig: &elbv2model.RedirectActionConfig{ + Path: awssdk.String("/#{path}"), + Host: &hostname, }, }, wantErr: false, @@ -106,17 +160,6 @@ func Test_buildHttpRedirectAction(t *testing.T) { want: nil, wantErr: true, }, - { - name: "path with wildcards in ReplacePrefixMatch", - filter: &gwv1.HTTPRequestRedirectFilter{ - Path: &gwv1.HTTPPathModifier{ - Type: gwv1.PrefixMatchHTTPPathModifier, - ReplacePrefixMatch: &invalidPath, - }, - }, - want: nil, - wantErr: true, - }, } for _, tt := range tests { diff --git a/pkg/gateway/routeutils/route_rule_transform.go b/pkg/gateway/routeutils/route_rule_transform.go index 8fdcaed5a..a6f82b49d 100644 --- a/pkg/gateway/routeutils/route_rule_transform.go +++ b/pkg/gateway/routeutils/route_rule_transform.go @@ -2,9 +2,10 @@ package routeutils import ( "fmt" + "strings" + elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2" gwv1 "sigs.k8s.io/gateway-api/apis/v1" - "strings" ) const ( @@ -35,6 +36,10 @@ func buildHTTPRuleTransforms(rule *gwv1.HTTPRouteRule, httpMatch *gwv1.HTTPRoute transforms = append(transforms, generateHostHeaderRewriteTransform(*rf.URLRewrite.Hostname)) } } + // Handle RequestRedirect with ReplacePrefixMatch as URLRewrite + if rf.RequestRedirect != nil && rf.RequestRedirect.Path != nil && rf.RequestRedirect.Path.ReplacePrefixMatch != nil { + transforms = append(transforms, generateURLRewritePathTransform(*rf.RequestRedirect.Path, httpMatch)) + } } } diff --git a/test/e2e/gateway/alb_instance_target_test.go b/test/e2e/gateway/alb_instance_target_test.go index e65d030f0..6d30e836a 100644 --- a/test/e2e/gateway/alb_instance_target_test.go +++ b/test/e2e/gateway/alb_instance_target_test.go @@ -473,7 +473,7 @@ var _ = Describe("test k8s alb gateway using instance targets reconciled by the httpExp := httpexpect.New(tf.LoggerReporter, fmt.Sprintf("http://%v", dnsName)) httpExp.GET("/api/v1/users").WithRedirectPolicy(httpexpect.DontFollowRedirects).Expect(). Status(302). - Header("Location").Equal("https://api.example.com:80/v2/*") + Header("Location").Equal("https://api.example.com:80/v2/v1/users") }) By("testing redirect with scheme and port change", func() { diff --git a/test/e2e/gateway/alb_ip_target_test.go b/test/e2e/gateway/alb_ip_target_test.go index fc61676dc..25aa68bb9 100644 --- a/test/e2e/gateway/alb_ip_target_test.go +++ b/test/e2e/gateway/alb_ip_target_test.go @@ -459,7 +459,7 @@ var _ = Describe("test k8s alb gateway using ip targets reconciled by the aws lo httpExp := httpexpect.New(tf.LoggerReporter, fmt.Sprintf("http://%v", dnsName)) httpExp.GET("/api/v1/users").WithRedirectPolicy(httpexpect.DontFollowRedirects).Expect(). Status(302). - Header("Location").Equal("https://api.example.com:80/v2/*") + Header("Location").Equal("https://api.example.com:80/v2/v1/users") }) By("testing redirect with scheme and port change", func() { From ffebb0bf0f212d633d13e1cd7cbeda50eeb319a2 Mon Sep 17 00:00:00 2001 From: shuqz Date: Thu, 20 Nov 2025 17:15:24 -0800 Subject: [PATCH 2/2] [feat gw-api]update behavior and doc --- docs/guide/gateway/l7gateway.md | 15 ++--- pkg/gateway/routeutils/route_rule_action.go | 21 ++----- .../routeutils/route_rule_action_test.go | 61 +------------------ 3 files changed, 13 insertions(+), 84 deletions(-) diff --git a/docs/guide/gateway/l7gateway.md b/docs/guide/gateway/l7gateway.md index 66466f47f..e40b8d3a1 100644 --- a/docs/guide/gateway/l7gateway.md +++ b/docs/guide/gateway/l7gateway.md @@ -206,9 +206,9 @@ The AWS Load Balancer Controller supports HTTPRoute RequestRedirect filters with **ReplacePrefixMatch Behavior:** -The behavior of `ReplacePrefixMatch` depends on whether other redirect components are modified: +We support `ReplacePrefixMatch` with limitations: -1. **With scheme/port/hostname changes** - Path suffixes are preserved: +1. **With scheme/port/hostname changes** - Works as expected: ```yaml filters: - type: RequestRedirect @@ -221,7 +221,7 @@ The behavior of `ReplacePrefixMatch` depends on whether other redirect component - Request: `/old-prefix/path/to/resource` - Redirects to: `/new-prefix/path/to/resource` ✅ (suffix preserved) -2. **Without other component changes** - Only prefix is replaced, suffixes are NOT preserved: +2. **Without other component changes** - AWS ALB will reject with redirect loop error: ```yaml filters: - type: RequestRedirect @@ -230,13 +230,14 @@ The behavior of `ReplacePrefixMatch` depends on whether other redirect component type: ReplacePrefixMatch replacePrefixMatch: /new-prefix ``` - - Request: `/old-prefix/path/to/resource` - - Redirects to: `/new-prefix` ❌ (suffix lost) + - This configuration will be rejected by the API with "InvalidLoadBalancerAction: The redirect configuration is not valid because it creates a loop." ❌ **Recommendations:** -- For path-only redirects with exact paths, use `ReplaceFullPath` -- To preserve path suffixes with prefix replacement, also modify `scheme`, `port`, or `hostname` +- For path-only redirects, use `ReplaceFullPath` instead +- To use `ReplacePrefixMatch`, you must also modify `scheme`, `port`, or `hostname` + +**Important**: If one HTTPRoute rule has an invalid redirect configuration (e.g., path-only redirect with `ReplacePrefixMatch` that cause redirect loop), the controller will fail to create that listener rule and stop processing subsequent rules in the same HTTPRoute. This means valid rules with lower precedence (shorter paths, later in the route) will not be created. #### Examples diff --git a/pkg/gateway/routeutils/route_rule_action.go b/pkg/gateway/routeutils/route_rule_action.go index b1c7ab7e9..6b8702de6 100644 --- a/pkg/gateway/routeutils/route_rule_action.go +++ b/pkg/gateway/routeutils/route_rule_action.go @@ -225,7 +225,6 @@ func buildHttpRuleRedirectActionsBasedOnFilter(filters []gwv1.HTTPRouteFilter, r // buildHttpRedirectAction configure filter attributes to RedirectActionConfig // gateway api has no attribute to specify query, use listener rule configuration func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectConfig *elbv2gw.RedirectActionConfig) (*elbv2model.Action, error) { - isComponentSpecified := false var statusCode string if filter.StatusCode != nil { statusCodeStr := fmt.Sprintf("HTTP_%d", *filter.StatusCode) @@ -236,7 +235,6 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon if filter.Port != nil { portStr := fmt.Sprintf("%d", *filter.Port) port = &portStr - isComponentSpecified = true } var protocol *string @@ -246,7 +244,6 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon return nil, errors.Errorf("unsupported redirect scheme: %v", upperScheme) } protocol = &upperScheme - isComponentSpecified = true } var path *string @@ -257,28 +254,18 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon return nil, errors.Errorf("ReplaceFullPath shouldn't contain wildcards: %v", pathValue) } path = filter.Path.ReplaceFullPath - isComponentSpecified = true } else if filter.Path.ReplacePrefixMatch != nil { - // Use #{path} if other components are modified (avoids redirect loop) - // Otherwise use literal prefix (no suffix preservation) - if filter.Scheme != nil || filter.Port != nil || filter.Hostname != nil { - pathVariable := "/#{path}" - path = &pathVariable - } else { - path = filter.Path.ReplacePrefixMatch + //url rewrite will handle path transform + pathValue := *filter.Path.ReplacePrefixMatch + if strings.ContainsAny(pathValue, "*?") { + return nil, errors.Errorf("ReplacePrefixMatch shouldn't contain wildcards: %v", pathValue) } - isComponentSpecified = true } } var hostname *string if filter.Hostname != nil { hostname = (*string)(filter.Hostname) - isComponentSpecified = true - } - - if !isComponentSpecified { - return nil, errors.Errorf("To avoid a redirect loop, you must modify at least one of the following components: protocol, port, hostname or path.") } var query *string diff --git a/pkg/gateway/routeutils/route_rule_action_test.go b/pkg/gateway/routeutils/route_rule_action_test.go index ae730fc4f..24431e5ec 100644 --- a/pkg/gateway/routeutils/route_rule_action_test.go +++ b/pkg/gateway/routeutils/route_rule_action_test.go @@ -66,59 +66,7 @@ func Test_buildHttpRedirectAction(t *testing.T) { wantErr: false, }, { - name: "redirect with prefix match only - uses literal prefix", - filter: &gwv1.HTTPRequestRedirectFilter{ - Path: &gwv1.HTTPPathModifier{ - Type: gwv1.PrefixMatchHTTPPathModifier, - ReplacePrefixMatch: &replacePrefixPath, - }, - }, - want: &elbv2model.Action{ - Type: elbv2model.ActionTypeRedirect, - RedirectConfig: &elbv2model.RedirectActionConfig{ - Path: &replacePrefixPath, - }, - }, - wantErr: false, - }, - { - name: "redirect with prefix match and scheme - uses #{path}", - filter: &gwv1.HTTPRequestRedirectFilter{ - Scheme: &scheme, - Path: &gwv1.HTTPPathModifier{ - Type: gwv1.PrefixMatchHTTPPathModifier, - ReplacePrefixMatch: &replacePrefixPath, - }, - }, - want: &elbv2model.Action{ - Type: elbv2model.ActionTypeRedirect, - RedirectConfig: &elbv2model.RedirectActionConfig{ - Path: awssdk.String("/#{path}"), - Protocol: &expectedScheme, - }, - }, - wantErr: false, - }, - { - name: "redirect with prefix match and port - uses #{path}", - filter: &gwv1.HTTPRequestRedirectFilter{ - Port: (*gwv1.PortNumber)(&port), - Path: &gwv1.HTTPPathModifier{ - Type: gwv1.PrefixMatchHTTPPathModifier, - ReplacePrefixMatch: &replacePrefixPath, - }, - }, - want: &elbv2model.Action{ - Type: elbv2model.ActionTypeRedirect, - RedirectConfig: &elbv2model.RedirectActionConfig{ - Path: awssdk.String("/#{path}"), - Port: &portString, - }, - }, - wantErr: false, - }, - { - name: "redirect with prefix match and hostname - uses #{path}", + name: "redirect with prefix - no path in redirect config", filter: &gwv1.HTTPRequestRedirectFilter{ Hostname: (*gwv1.PreciseHostname)(&hostname), Path: &gwv1.HTTPPathModifier{ @@ -129,18 +77,11 @@ func Test_buildHttpRedirectAction(t *testing.T) { want: &elbv2model.Action{ Type: elbv2model.ActionTypeRedirect, RedirectConfig: &elbv2model.RedirectActionConfig{ - Path: awssdk.String("/#{path}"), Host: &hostname, }, }, wantErr: false, }, - { - name: "redirect with no component provided", - filter: &gwv1.HTTPRequestRedirectFilter{}, - want: nil, - wantErr: true, - }, { name: "invalid scheme provided", filter: &gwv1.HTTPRequestRedirectFilter{