From eb0295c971b5926dddc37252d4ba30abff67e14f Mon Sep 17 00:00:00 2001
From: Saylor Berman <s.berman@f5.com>
Date: Wed, 20 Dec 2023 08:14:45 -0700
Subject: [PATCH 1/2] Support for URL Rewrite filter

Problem: As a user, I want to be able to configure URL rewrites for hostname and/or path rewrites on the server side.

Solution: Using the HTTPRoute filter API, a user can now configure a hostname and/or path-based rewrite. Hostname rewrite will update the Host header, while a path rewrite utilizes nginx's `rewrite` directive.

Enabled conformance tests for these features and added how-to guides.
---
 conformance/Makefile                          |   2 +-
 .../mode/static/nginx/config/http/config.go   |   1 +
 internal/mode/static/nginx/config/servers.go  | 199 +++++--
 .../static/nginx/config/servers_template.go   |  10 +-
 .../mode/static/nginx/config/servers_test.go  | 508 +++++++++++++++---
 .../static/nginx/config/validation/common.go  |  12 +-
 .../nginx/config/validation/http_filters.go   |  40 +-
 .../config/validation/http_filters_test.go    |  53 +-
 .../nginx/config/validation/http_njs_match.go |  11 -
 .../nginx/config/validation/http_validator.go |   1 +
 .../static/nginx/modules/src/httpmatches.js   |  11 +-
 .../nginx/modules/test/httpmatches.test.js    |   2 +-
 .../static/state/dataplane/configuration.go   |   5 +
 .../state/dataplane/configuration_test.go     |  20 +-
 .../mode/static/state/dataplane/convert.go    |  26 +
 .../static/state/dataplane/convert_test.go    |  57 ++
 internal/mode/static/state/dataplane/types.go |  29 +
 internal/mode/static/state/graph/httproute.go |  49 +-
 .../mode/static/state/graph/httproute_test.go | 186 ++++++-
 .../fake_httpfields_validator.go              | 222 +++++---
 .../mode/static/state/validation/validator.go |   3 +-
 21 files changed, 1197 insertions(+), 250 deletions(-)

diff --git a/conformance/Makefile b/conformance/Makefile
index 9e19179450..27819aefc3 100644
--- a/conformance/Makefile
+++ b/conformance/Makefile
@@ -3,7 +3,7 @@ PREFIX = nginx-gateway-fabric
 NGINX_PREFIX = $(PREFIX)/nginx
 GW_API_VERSION ?= 1.0.0
 GATEWAY_CLASS = nginx
-SUPPORTED_FEATURES = HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,GatewayClassObservedGenerationBump
+SUPPORTED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080
 KIND_IMAGE ?= $(shell grep -m1 'FROM kindest/node' <tests/Dockerfile | awk -F'[ ]' '{print $$2}')
 KIND_KUBE_CONFIG=$${HOME}/.kube/kind/config
 CONFORMANCE_TAG = latest
diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go
index fa917e58f7..f99b3a6429 100644
--- a/internal/mode/static/nginx/config/http/config.go
+++ b/internal/mode/static/nginx/config/http/config.go
@@ -13,6 +13,7 @@ type Server struct {
 // Location holds all configuration for an HTTP location.
 type Location struct {
 	Return          *Return
+	Rewrites        []string
 	Path            string
 	ProxyPass       string
 	HTTPMatchVar    string
diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go
index 9b77d71ba0..58e2524929 100644
--- a/internal/mode/static/nginx/config/servers.go
+++ b/internal/mode/static/nginx/config/servers.go
@@ -18,6 +18,26 @@ const (
 	rootPath             = "/"
 )
 
+// baseHeaders contains the constant headers set in each server block
+var baseHeaders = []http.Header{
+	{
+		Name:  "Host",
+		Value: "$gw_api_compliant_host",
+	},
+	{
+		Name:  "X-Forwarded-For",
+		Value: "$proxy_add_x_forwarded_for",
+	},
+	{
+		Name:  "Upgrade",
+		Value: "$http_upgrade",
+	},
+	{
+		Name:  "Connection",
+		Value: "$connection_upgrade",
+	},
+}
+
 func executeServers(conf dataplane.Configuration) []byte {
 	servers := createServers(conf.HTTPServers, conf.SSLServers)
 
@@ -72,6 +92,15 @@ func createServer(virtualServer dataplane.VirtualServer) http.Server {
 	}
 }
 
+// rewriteConfig contains the configuration for a location to rewrite paths,
+// as specified in a URLRewrite filter
+type rewriteConfig struct {
+	// InternalRewrite rewrites an internal URI to the original URI (ex: /coffee_prefix_route0 -> /coffee)
+	InternalRewrite string
+	// MainRewrite rewrites the original URI to the new URI (ex: /coffee -> /beans)
+	MainRewrite string
+}
+
 func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.Location {
 	maxLocs, pathsAndTypes := getMaxLocationCountAndPathMap(pathRules)
 	locs := make([]http.Location, 0, maxLocs)
@@ -94,42 +123,7 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
 				matches = append(matches, match)
 			}
 
-			if r.Filters.InvalidFilter != nil {
-				for i := range buildLocations {
-					buildLocations[i].Return = &http.Return{Code: http.StatusInternalServerError}
-				}
-				locs = append(locs, buildLocations...)
-				continue
-			}
-
-			// There could be a case when the filter has the type set but not the corresponding field.
-			// For example, type is v1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil.
-			// The imported Webhook validation webhook catches that.
-
-			// FIXME(pleshakov): Ensure dataplane.Configuration -related types don't include v1 types, so that
-			// we don't need to make any assumptions like above here. After fixing this, ensure that there is a test
-			// for checking the imported Webhook validation catches the case above.
-			// https://github.com/nginxinc/nginx-gateway-fabric/issues/660
-
-			// RequestRedirect and proxying are mutually exclusive.
-			if r.Filters.RequestRedirect != nil {
-				ret := createReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)
-				for i := range buildLocations {
-					buildLocations[i].Return = ret
-				}
-				locs = append(locs, buildLocations...)
-				continue
-			}
-
-			proxySetHeaders := generateProxySetHeaders(r.Filters.RequestHeaderModifiers)
-			for i := range buildLocations {
-				buildLocations[i].ProxySetHeaders = proxySetHeaders
-			}
-
-			proxyPass := createProxyPass(r.BackendGroup)
-			for i := range buildLocations {
-				buildLocations[i].ProxyPass = proxyPass
-			}
+			buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, listenerPort, rule.Path)
 			locs = append(locs, buildLocations...)
 		}
 
@@ -230,6 +224,48 @@ func initializeInternalLocation(
 	return createMatchLocation(path), createHTTPMatch(match, path)
 }
 
+// updateLocationsForFilters updates the existing locations with any relevant filters.
+func updateLocationsForFilters(
+	filters dataplane.HTTPFilters,
+	buildLocations []http.Location,
+	matchRule dataplane.MatchRule,
+	listenerPort int32,
+	path string,
+) []http.Location {
+	if filters.InvalidFilter != nil {
+		for i := range buildLocations {
+			buildLocations[i].Return = &http.Return{Code: http.StatusInternalServerError}
+		}
+		return buildLocations
+	}
+
+	if filters.RequestRedirect != nil {
+		ret := createReturnValForRedirectFilter(filters.RequestRedirect, listenerPort)
+		for i := range buildLocations {
+			buildLocations[i].Return = ret
+		}
+		return buildLocations
+	}
+
+	rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path)
+	proxySetHeaders := generateProxySetHeaders(&matchRule.Filters)
+	proxyPass := createProxyPass(matchRule.BackendGroup, matchRule.Filters.RequestURLRewrite)
+	for i := range buildLocations {
+		if rewrites != nil {
+			if buildLocations[i].Internal && rewrites.InternalRewrite != "" {
+				buildLocations[i].Rewrites = append(buildLocations[i].Rewrites, rewrites.InternalRewrite)
+			}
+			if rewrites.MainRewrite != "" {
+				buildLocations[i].Rewrites = append(buildLocations[i].Rewrites, rewrites.MainRewrite)
+			}
+		}
+		buildLocations[i].ProxySetHeaders = proxySetHeaders
+		buildLocations[i].ProxyPass = proxyPass
+	}
+
+	return buildLocations
+}
+
 func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilter, listenerPort int32) *http.Return {
 	if filter == nil {
 		return nil
@@ -275,6 +311,49 @@ func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilte
 	}
 }
 
+func createRewritesValForRewriteFilter(filter *dataplane.HTTPURLRewriteFilter, path string) *rewriteConfig {
+	if filter == nil {
+		return nil
+	}
+
+	rewrites := &rewriteConfig{}
+
+	if filter.Path != nil {
+		rewrites.InternalRewrite = "^ $request_uri"
+		switch filter.Path.Type {
+		case dataplane.ReplaceFullPath:
+			rewrites.MainRewrite = fmt.Sprintf("^ %s break", filter.Path.Replacement)
+		case dataplane.ReplacePrefixMatch:
+			filterPrefix := filter.Path.Replacement
+			if filterPrefix == "" {
+				filterPrefix = "/"
+			}
+
+			// capture everything after the configured prefix
+			regex := fmt.Sprintf("^%s(.*)$", path)
+			// replace the configured prefix with the filter prefix and append what was captured
+			replacement := fmt.Sprintf("%s$1", filterPrefix)
+
+			// if configured prefix does not end in /, but replacement prefix does end in /,
+			// then make sure that we *require* but *don't capture* a trailing slash in the request,
+			// otherwise we'll get duplicate slashes in the full replacement
+			if strings.HasSuffix(filterPrefix, "/") && !strings.HasSuffix(path, "/") {
+				regex = fmt.Sprintf("^%s(?:/(.*))?$", path)
+			}
+
+			// if configured prefix ends in / we won't capture it for a request (since it's not in the regex),
+			// so append it to the replacement prefix if the replacement prefix doesn't already end in /
+			if strings.HasSuffix(path, "/") && !strings.HasSuffix(filterPrefix, "/") {
+				replacement = fmt.Sprintf("%s/$1", filterPrefix)
+			}
+
+			rewrites.MainRewrite = fmt.Sprintf("%s %s break", regex, replacement)
+		}
+	}
+
+	return rewrites
+}
+
 // httpMatch is an internal representation of an HTTPRouteMatch.
 // This struct is marshaled into a string and stored as a variable in the nginx location block for the route's path.
 // The NJS httpmatches module will look up this variable on the request object and compare the request against the
@@ -354,13 +433,18 @@ func isPathOnlyMatch(match dataplane.Match) bool {
 	return match.Method == nil && len(match.Headers) == 0 && len(match.QueryParams) == 0
 }
 
-func createProxyPass(backendGroup dataplane.BackendGroup) string {
+func createProxyPass(backendGroup dataplane.BackendGroup, filter *dataplane.HTTPURLRewriteFilter) string {
+	var requestURI string
+	if filter == nil || filter.Path == nil {
+		requestURI = "$request_uri"
+	}
+
 	backendName := backendGroupName(backendGroup)
 	if backendGroupNeedsSplit(backendGroup) {
-		return "http://$" + convertStringToSafeVariableName(backendName)
+		return "http://$" + convertStringToSafeVariableName(backendName) + requestURI
 	}
 
-	return "http://" + backendName
+	return "http://" + backendName + requestURI
 }
 
 func createMatchLocation(path string) http.Location {
@@ -370,27 +454,44 @@ func createMatchLocation(path string) http.Location {
 	}
 }
 
-func generateProxySetHeaders(filters *dataplane.HTTPHeaderFilter) []http.Header {
-	if filters == nil {
-		return nil
+func generateProxySetHeaders(filters *dataplane.HTTPFilters) []http.Header {
+	headers := make([]http.Header, len(baseHeaders))
+	copy(headers, baseHeaders)
+
+	if filters != nil && filters.RequestURLRewrite != nil && filters.RequestURLRewrite.Hostname != nil {
+		for i, header := range headers {
+			if header.Name == "Host" {
+				headers[i].Value = *filters.RequestURLRewrite.Hostname
+				break
+			}
+		}
+	}
+
+	if filters == nil || filters.RequestHeaderModifiers == nil {
+		return headers
 	}
-	proxySetHeaders := make([]http.Header, 0, len(filters.Add)+len(filters.Set)+len(filters.Remove))
-	if len(filters.Add) > 0 {
-		addHeaders := convertAddHeaders(filters.Add)
+
+	headerFilter := filters.RequestHeaderModifiers
+
+	headerLen := len(headerFilter.Add) + len(headerFilter.Set) + len(headerFilter.Remove) + len(headers)
+	proxySetHeaders := make([]http.Header, 0, headerLen)
+	if len(headerFilter.Add) > 0 {
+		addHeaders := convertAddHeaders(headerFilter.Add)
 		proxySetHeaders = append(proxySetHeaders, addHeaders...)
 	}
-	if len(filters.Set) > 0 {
-		setHeaders := convertSetHeaders(filters.Set)
+	if len(headerFilter.Set) > 0 {
+		setHeaders := convertSetHeaders(headerFilter.Set)
 		proxySetHeaders = append(proxySetHeaders, setHeaders...)
 	}
 	// If the value of a header field is an empty string then this field will not be passed to a proxied server
-	for _, h := range filters.Remove {
+	for _, h := range headerFilter.Remove {
 		proxySetHeaders = append(proxySetHeaders, http.Header{
 			Name:  h,
 			Value: "",
 		})
 	}
-	return proxySetHeaders
+
+	return append(proxySetHeaders, headers...)
 }
 
 func convertAddHeaders(headers []dataplane.HTTPHeader) []http.Header {
diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go
index 38321d9f42..7574efb1b8 100644
--- a/internal/mode/static/nginx/config/servers_template.go
+++ b/internal/mode/static/nginx/config/servers_template.go
@@ -37,6 +37,10 @@ server {
         internal;
         {{ end }}
 
+        {{- range $r := $l.Rewrites }}
+        rewrite {{ $r }};
+        {{- end }}
+
         {{- if $l.Return -}}
         return {{ $l.Return.Code }} "{{ $l.Return.Body }}";
         {{ end }}
@@ -50,12 +54,8 @@ server {
             {{ range $h := $l.ProxySetHeaders }}
         proxy_set_header {{ $h.Name }} "{{ $h.Value }}";
             {{- end }}
-        proxy_set_header Host $gw_api_compliant_host;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_http_version 1.1;
-        proxy_set_header Upgrade $http_upgrade;
-        proxy_set_header Connection $connection_upgrade;
-        proxy_pass {{ $l.ProxyPass }}$request_uri;
+        proxy_pass {{ $l.ProxyPass }};
         {{- end }}
     }
         {{ end }}
diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go
index 3cb1a1c5ca..91d3f9aae7 100644
--- a/internal/mode/static/nginx/config/servers_test.go
+++ b/internal/mode/static/nginx/config/servers_test.go
@@ -335,6 +335,51 @@ func TestCreateServers(t *testing.T) {
 				},
 			},
 		},
+		{
+			Path:     "/rewrite",
+			PathType: dataplane.PathTypePrefix,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match: dataplane.Match{},
+					Filters: dataplane.HTTPFilters{
+						RequestURLRewrite: &dataplane.HTTPURLRewriteFilter{
+							Hostname: helpers.GetPointer("new.example.com"),
+							Path: &dataplane.HTTPPathModifier{
+								Type:        dataplane.ReplaceFullPath,
+								Replacement: "/replacement",
+							},
+						},
+					},
+					BackendGroup: fooGroup,
+				},
+			},
+		},
+		{
+			Path:     "/rewrite-with-headers",
+			PathType: dataplane.PathTypePrefix,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match: dataplane.Match{
+						Headers: []dataplane.HTTPHeaderMatch{
+							{
+								Name:  "rewrite",
+								Value: "this",
+							},
+						},
+					},
+					Filters: dataplane.HTTPFilters{
+						RequestURLRewrite: &dataplane.HTTPURLRewriteFilter{
+							Hostname: helpers.GetPointer("new.example.com"),
+							Path: &dataplane.HTTPPathModifier{
+								Type:        dataplane.ReplacePrefixMatch,
+								Replacement: "/prefix-replacement",
+							},
+						},
+					},
+					BackendGroup: fooGroup,
+				},
+			},
+		},
 		{
 			Path:     "/invalid-filter",
 			PathType: dataplane.PathTypePrefix,
@@ -469,6 +514,30 @@ func TestCreateServers(t *testing.T) {
 			RedirectPath: "/redirect-with-headers_prefix_route0",
 		},
 	}
+	rewriteHeaderMatches := []httpMatch{
+		{
+			Headers:      []string{"rewrite:this"},
+			RedirectPath: "/rewrite-with-headers_prefix_route0",
+		},
+	}
+	rewriteProxySetHeaders := []http.Header{
+		{
+			Name:  "Host",
+			Value: "new.example.com",
+		},
+		{
+			Name:  "X-Forwarded-For",
+			Value: "$proxy_add_x_forwarded_for",
+		},
+		{
+			Name:  "Upgrade",
+			Value: "$http_upgrade",
+		},
+		{
+			Name:  "Connection",
+			Value: "$connection_upgrade",
+		},
+	}
 	invalidFilterHeaderMatches := []httpMatch{
 		{
 			Headers:      []string{"filter:this"},
@@ -484,40 +553,46 @@ func TestCreateServers(t *testing.T) {
 
 		return []http.Location{
 			{
-				Path:      "/_prefix_route0",
-				Internal:  true,
-				ProxyPass: "http://test_foo_80",
+				Path:            "/_prefix_route0",
+				Internal:        true,
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: baseHeaders,
 			},
 			{
-				Path:      "/_prefix_route1",
-				Internal:  true,
-				ProxyPass: "http://test_foo_80",
+				Path:            "/_prefix_route1",
+				Internal:        true,
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: baseHeaders,
 			},
 			{
-				Path:      "/_prefix_route2",
-				Internal:  true,
-				ProxyPass: "http://test_foo_80",
+				Path:            "/_prefix_route2",
+				Internal:        true,
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: baseHeaders,
 			},
 			{
 				Path:         "/",
 				HTTPMatchVar: expectedMatchString(slashMatches),
 			},
 			{
-				Path:      "/test_prefix_route0",
-				Internal:  true,
-				ProxyPass: "http://$test__route1_rule1",
+				Path:            "/test_prefix_route0",
+				Internal:        true,
+				ProxyPass:       "http://$test__route1_rule1$request_uri",
+				ProxySetHeaders: baseHeaders,
 			},
 			{
 				Path:         "/test/",
 				HTTPMatchVar: expectedMatchString(testMatches),
 			},
 			{
-				Path:      "/path-only/",
-				ProxyPass: "http://invalid-backend-ref",
+				Path:            "/path-only/",
+				ProxyPass:       "http://invalid-backend-ref$request_uri",
+				ProxySetHeaders: baseHeaders,
 			},
 			{
-				Path:      "= /path-only",
-				ProxyPass: "http://invalid-backend-ref",
+				Path:            "= /path-only",
+				ProxyPass:       "http://invalid-backend-ref$request_uri",
+				ProxySetHeaders: baseHeaders,
 			},
 			{
 				Path: "/redirect-implicit-port/",
@@ -563,6 +638,33 @@ func TestCreateServers(t *testing.T) {
 				Path:         "= /redirect-with-headers",
 				HTTPMatchVar: expectedMatchString(redirectHeaderMatches),
 			},
+			{
+				Path:            "/rewrite/",
+				Rewrites:        []string{"^ /replacement break"},
+				ProxyPass:       "http://test_foo_80",
+				ProxySetHeaders: rewriteProxySetHeaders,
+			},
+			{
+				Path:            "= /rewrite",
+				Rewrites:        []string{"^ /replacement break"},
+				ProxyPass:       "http://test_foo_80",
+				ProxySetHeaders: rewriteProxySetHeaders,
+			},
+			{
+				Path:            "/rewrite-with-headers_prefix_route0",
+				Rewrites:        []string{"^ $request_uri", "^/rewrite-with-headers(.*)$ /prefix-replacement$1 break"},
+				Internal:        true,
+				ProxyPass:       "http://test_foo_80",
+				ProxySetHeaders: rewriteProxySetHeaders,
+			},
+			{
+				Path:         "/rewrite-with-headers/",
+				HTTPMatchVar: expectedMatchString(rewriteHeaderMatches),
+			},
+			{
+				Path:         "= /rewrite-with-headers",
+				HTTPMatchVar: expectedMatchString(rewriteHeaderMatches),
+			},
 			{
 				Path: "/invalid-filter/",
 				Return: &http.Return{
@@ -591,13 +693,15 @@ func TestCreateServers(t *testing.T) {
 				HTTPMatchVar: expectedMatchString(invalidFilterHeaderMatches),
 			},
 			{
-				Path:      "= /exact",
-				ProxyPass: "http://test_foo_80",
+				Path:            "= /exact",
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: baseHeaders,
 			},
 			{
-				Path:      "/test_exact_route0",
-				ProxyPass: "http://test_foo_80",
-				Internal:  true,
+				Path:            "/test_exact_route0",
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: baseHeaders,
+				Internal:        true,
 			},
 			{
 				Path:         "= /test",
@@ -605,22 +709,54 @@ func TestCreateServers(t *testing.T) {
 			},
 			{
 				Path:      "/proxy-set-headers/",
-				ProxyPass: "http://test_foo_80",
+				ProxyPass: "http://test_foo_80$request_uri",
 				ProxySetHeaders: []http.Header{
 					{
 						Name:  "my-header",
 						Value: "${my_header_header_var}some-value-123",
 					},
+					{
+						Name:  "Host",
+						Value: "$gw_api_compliant_host",
+					},
+					{
+						Name:  "X-Forwarded-For",
+						Value: "$proxy_add_x_forwarded_for",
+					},
+					{
+						Name:  "Upgrade",
+						Value: "$http_upgrade",
+					},
+					{
+						Name:  "Connection",
+						Value: "$connection_upgrade",
+					},
 				},
 			},
 			{
 				Path:      "= /proxy-set-headers",
-				ProxyPass: "http://test_foo_80",
+				ProxyPass: "http://test_foo_80$request_uri",
 				ProxySetHeaders: []http.Header{
 					{
 						Name:  "my-header",
 						Value: "${my_header_header_var}some-value-123",
 					},
+					{
+						Name:  "Host",
+						Value: "$gw_api_compliant_host",
+					},
+					{
+						Name:  "X-Forwarded-For",
+						Value: "$proxy_add_x_forwarded_for",
+					},
+					{
+						Name:  "Upgrade",
+						Value: "$http_upgrade",
+					},
+					{
+						Name:  "Connection",
+						Value: "$connection_upgrade",
+					},
 				},
 			},
 		}
@@ -725,12 +861,14 @@ func TestCreateServersConflicts(t *testing.T) {
 			},
 			expLocs: []http.Location{
 				{
-					Path:      "/coffee/",
-					ProxyPass: "http://test_foo_80",
+					Path:            "/coffee/",
+					ProxyPass:       "http://test_foo_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				{
-					Path:      "= /coffee",
-					ProxyPass: "http://test_bar_80",
+					Path:            "= /coffee",
+					ProxyPass:       "http://test_bar_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				createDefaultRootLocation(),
 			},
@@ -761,12 +899,14 @@ func TestCreateServersConflicts(t *testing.T) {
 			},
 			expLocs: []http.Location{
 				{
-					Path:      "= /coffee",
-					ProxyPass: "http://test_foo_80",
+					Path:            "= /coffee",
+					ProxyPass:       "http://test_foo_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				{
-					Path:      "/coffee/",
-					ProxyPass: "http://test_bar_80",
+					Path:            "/coffee/",
+					ProxyPass:       "http://test_bar_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				createDefaultRootLocation(),
 			},
@@ -807,12 +947,14 @@ func TestCreateServersConflicts(t *testing.T) {
 			},
 			expLocs: []http.Location{
 				{
-					Path:      "/coffee/",
-					ProxyPass: "http://test_bar_80",
+					Path:            "/coffee/",
+					ProxyPass:       "http://test_bar_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				{
-					Path:      "= /coffee",
-					ProxyPass: "http://test_baz_80",
+					Path:            "= /coffee",
+					ProxyPass:       "http://test_baz_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				createDefaultRootLocation(),
 			},
@@ -916,12 +1058,14 @@ func TestCreateLocationsRootPath(t *testing.T) {
 			pathRules: getPathRules(false /* rootPath */),
 			expLocations: []http.Location{
 				{
-					Path:      "/path-1",
-					ProxyPass: "http://test_foo_80",
+					Path:            "/path-1",
+					ProxyPass:       "http://test_foo_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				{
-					Path:      "/path-2",
-					ProxyPass: "http://test_foo_80",
+					Path:            "/path-2",
+					ProxyPass:       "http://test_foo_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				{
 					Path: "/",
@@ -936,16 +1080,19 @@ func TestCreateLocationsRootPath(t *testing.T) {
 			pathRules: getPathRules(true /* rootPath */),
 			expLocations: []http.Location{
 				{
-					Path:      "/path-1",
-					ProxyPass: "http://test_foo_80",
+					Path:            "/path-1",
+					ProxyPass:       "http://test_foo_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				{
-					Path:      "/path-2",
-					ProxyPass: "http://test_foo_80",
+					Path:            "/path-2",
+					ProxyPass:       "http://test_foo_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 				{
-					Path:      "/",
-					ProxyPass: "http://test_foo_80",
+					Path:            "/",
+					ProxyPass:       "http://test_foo_80$request_uri",
+					ProxySetHeaders: baseHeaders,
 				},
 			},
 		},
@@ -1102,6 +1249,132 @@ func TestCreateReturnValForRedirectFilter(t *testing.T) {
 	}
 }
 
+func TestCreateRewritesValForRewriteFilter(t *testing.T) {
+	tests := []struct {
+		filter   *dataplane.HTTPURLRewriteFilter
+		expected *rewriteConfig
+		msg      string
+		path     string
+	}{
+		{
+			filter:   nil,
+			expected: nil,
+			msg:      "filter is nil",
+		},
+		{
+			filter:   &dataplane.HTTPURLRewriteFilter{},
+			expected: &rewriteConfig{},
+			msg:      "all fields are empty",
+		},
+		{
+			filter: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{
+					Type:        dataplane.ReplaceFullPath,
+					Replacement: "/full-path",
+				},
+			},
+			expected: &rewriteConfig{
+				InternalRewrite: "^ $request_uri",
+				MainRewrite:     "^ /full-path break",
+			},
+			msg: "full path",
+		},
+		{
+			path: "/original",
+			filter: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{
+					Type:        dataplane.ReplacePrefixMatch,
+					Replacement: "/prefix-path",
+				},
+			},
+			expected: &rewriteConfig{
+				InternalRewrite: "^ $request_uri",
+				MainRewrite:     "^/original(.*)$ /prefix-path$1 break",
+			},
+			msg: "prefix path no trailing slashes",
+		},
+		{
+			path: "/original",
+			filter: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{
+					Type:        dataplane.ReplacePrefixMatch,
+					Replacement: "",
+				},
+			},
+			expected: &rewriteConfig{
+				InternalRewrite: "^ $request_uri",
+				MainRewrite:     "^/original(?:/(.*))?$ /$1 break",
+			},
+			msg: "prefix path empty string",
+		},
+		{
+			path: "/original",
+			filter: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{
+					Type:        dataplane.ReplacePrefixMatch,
+					Replacement: "/",
+				},
+			},
+			expected: &rewriteConfig{
+				InternalRewrite: "^ $request_uri",
+				MainRewrite:     "^/original(?:/(.*))?$ /$1 break",
+			},
+			msg: "prefix path /",
+		},
+		{
+			path: "/original",
+			filter: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{
+					Type:        dataplane.ReplacePrefixMatch,
+					Replacement: "/trailing/",
+				},
+			},
+			expected: &rewriteConfig{
+				InternalRewrite: "^ $request_uri",
+				MainRewrite:     "^/original(?:/(.*))?$ /trailing/$1 break",
+			},
+			msg: "prefix path replacement with trailing /",
+		},
+		{
+			path: "/original/",
+			filter: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{
+					Type:        dataplane.ReplacePrefixMatch,
+					Replacement: "/prefix-path",
+				},
+			},
+			expected: &rewriteConfig{
+				InternalRewrite: "^ $request_uri",
+				MainRewrite:     "^/original/(.*)$ /prefix-path/$1 break",
+			},
+			msg: "prefix path original with trailing /",
+		},
+		{
+			path: "/original/",
+			filter: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{
+					Type:        dataplane.ReplacePrefixMatch,
+					Replacement: "/trailing/",
+				},
+			},
+			expected: &rewriteConfig{
+				InternalRewrite: "^ $request_uri",
+				MainRewrite:     "^/original/(.*)$ /trailing/$1 break",
+			},
+			msg: "prefix path both with trailing slashes",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.msg, func(t *testing.T) {
+			g := NewWithT(t)
+
+			result := createRewritesValForRewriteFilter(test.filter, test.path)
+			g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty())
+		})
+	}
+}
+
 func TestCreateHTTPMatch(t *testing.T) {
 	testPath := "/internal_loc"
 
@@ -1360,11 +1633,12 @@ func TestCreateProxyPass(t *testing.T) {
 	g := NewWithT(t)
 
 	tests := []struct {
+		rewrite  *dataplane.HTTPURLRewriteFilter
 		expected string
 		grp      dataplane.BackendGroup
 	}{
 		{
-			expected: "http://10.0.0.1:80",
+			expected: "http://10.0.0.1:80$request_uri",
 			grp: dataplane.BackendGroup{
 				Backends: []dataplane.Backend{
 					{
@@ -1376,7 +1650,7 @@ func TestCreateProxyPass(t *testing.T) {
 			},
 		},
 		{
-			expected: "http://$ns1__bg_rule0",
+			expected: "http://$ns1__bg_rule0$request_uri",
 			grp: dataplane.BackendGroup{
 				Source: types.NamespacedName{Namespace: "ns1", Name: "bg"},
 				Backends: []dataplane.Backend{
@@ -1393,10 +1667,25 @@ func TestCreateProxyPass(t *testing.T) {
 				},
 			},
 		},
+		{
+			expected: "http://10.0.0.1:80",
+			rewrite: &dataplane.HTTPURLRewriteFilter{
+				Path: &dataplane.HTTPPathModifier{},
+			},
+			grp: dataplane.BackendGroup{
+				Backends: []dataplane.Backend{
+					{
+						UpstreamName: "10.0.0.1:80",
+						Valid:        true,
+						Weight:       1,
+					},
+				},
+			},
+		},
 	}
 
 	for _, tc := range tests {
-		result := createProxyPass(tc.grp)
+		result := createProxyPass(tc.grp, tc.rewrite)
 		g.Expect(result).To(Equal(tc.expected))
 	}
 }
@@ -1438,38 +1727,107 @@ func TestCreatePathForMatch(t *testing.T) {
 }
 
 func TestGenerateProxySetHeaders(t *testing.T) {
-	g := NewWithT(t)
-
-	filters := dataplane.HTTPHeaderFilter{
-		Add: []dataplane.HTTPHeader{
-			{
-				Name:  "Authorization",
-				Value: "my-auth",
+	tests := []struct {
+		filters         *dataplane.HTTPFilters
+		msg             string
+		expectedHeaders []http.Header
+	}{
+		{
+			msg: "header filter",
+			filters: &dataplane.HTTPFilters{
+				RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{
+					Add: []dataplane.HTTPHeader{
+						{
+							Name:  "Authorization",
+							Value: "my-auth",
+						},
+					},
+					Set: []dataplane.HTTPHeader{
+						{
+							Name:  "Accept-Encoding",
+							Value: "gzip",
+						},
+					},
+					Remove: []string{"my-header"},
+				},
 			},
-		},
-		Set: []dataplane.HTTPHeader{
-			{
-				Name:  "Accept-Encoding",
-				Value: "gzip",
+			expectedHeaders: []http.Header{
+				{
+					Name:  "Authorization",
+					Value: "${authorization_header_var}my-auth",
+				},
+				{
+					Name:  "Accept-Encoding",
+					Value: "gzip",
+				},
+				{
+					Name:  "my-header",
+					Value: "",
+				},
+				{
+					Name:  "Host",
+					Value: "$gw_api_compliant_host",
+				},
+				{
+					Name:  "X-Forwarded-For",
+					Value: "$proxy_add_x_forwarded_for",
+				},
+				{
+					Name:  "Upgrade",
+					Value: "$http_upgrade",
+				},
+				{
+					Name:  "Connection",
+					Value: "$connection_upgrade",
+				},
 			},
 		},
-		Remove: []string{"my-header"},
-	}
-	expectedHeaders := []http.Header{
-		{
-			Name:  "Authorization",
-			Value: "${authorization_header_var}my-auth",
-		},
-		{
-			Name:  "Accept-Encoding",
-			Value: "gzip",
-		},
 		{
-			Name:  "my-header",
-			Value: "",
+			msg: "with url rewrite hostname",
+			filters: &dataplane.HTTPFilters{
+				RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{
+					Add: []dataplane.HTTPHeader{
+						{
+							Name:  "Authorization",
+							Value: "my-auth",
+						},
+					},
+				},
+				RequestURLRewrite: &dataplane.HTTPURLRewriteFilter{
+					Hostname: helpers.GetPointer("rewrite-hostname"),
+				},
+			},
+			expectedHeaders: []http.Header{
+				{
+					Name:  "Authorization",
+					Value: "${authorization_header_var}my-auth",
+				},
+				{
+					Name:  "Host",
+					Value: "rewrite-hostname",
+				},
+				{
+					Name:  "X-Forwarded-For",
+					Value: "$proxy_add_x_forwarded_for",
+				},
+				{
+					Name:  "Upgrade",
+					Value: "$http_upgrade",
+				},
+				{
+					Name:  "Connection",
+					Value: "$connection_upgrade",
+				},
+			},
 		},
 	}
 
-	headers := generateProxySetHeaders(&filters)
-	g.Expect(headers).To(Equal(expectedHeaders))
+	for _, tc := range tests {
+		t.Run(tc.msg, func(t *testing.T) {
+			g := NewWithT(t)
+
+			headers := generateProxySetHeaders(tc.filters)
+			g.Expect(headers).To(Equal(tc.expectedHeaders))
+		})
+	}
 }
diff --git a/internal/mode/static/nginx/config/validation/common.go b/internal/mode/static/nginx/config/validation/common.go
index fc98931da6..bca6a97095 100644
--- a/internal/mode/static/nginx/config/validation/common.go
+++ b/internal/mode/static/nginx/config/validation/common.go
@@ -8,6 +8,16 @@ import (
 	k8svalidation "k8s.io/apimachinery/pkg/util/validation"
 )
 
+const (
+	pathFmt    = `/[^\s{};]*`
+	pathErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`"
+)
+
+var (
+	pathRegexp   = regexp.MustCompile("^" + pathFmt + "$")
+	pathExamples = []string{"/", "/path", "/path/subpath-123"}
+)
+
 const (
 	escapedStringsFmt    = `([^"\\]|\\.)*`
 	escapedStringsErrMsg = `must have all '"' (double quotes) escaped and must not end with an unescaped '\' ` +
@@ -30,7 +40,7 @@ func validateEscapedString(value string, examples []string) error {
 
 const (
 	escapedStringsNoVarExpansionFmt           = `([^"$\\]|\\[^$])*`
-	escapedStringsNoVarExpansionErrMsg string = `a valid header must have all '"' escaped and must not contain any ` +
+	escapedStringsNoVarExpansionErrMsg string = `a valid value must have all '"' escaped and must not contain any ` +
 		`'$' or end with an unescaped '\'`
 )
 
diff --git a/internal/mode/static/nginx/config/validation/http_filters.go b/internal/mode/static/nginx/config/validation/http_filters.go
index e505f94276..3fc638108e 100644
--- a/internal/mode/static/nginx/config/validation/http_filters.go
+++ b/internal/mode/static/nginx/config/validation/http_filters.go
@@ -1,9 +1,19 @@
 package validation
 
+import (
+	"errors"
+	"strings"
+
+	k8svalidation "k8s.io/apimachinery/pkg/util/validation"
+)
+
 // HTTPRedirectValidator validates values for a redirect, which in NGINX is done with the return directive.
 // For example, return 302 "https://example.com:8080";
 type HTTPRedirectValidator struct{}
 
+// HTTPURLRewriteValidator validates values for a URL rewrite.
+type HTTPURLRewriteValidator struct{}
+
 // HTTPRequestHeaderValidator validates values for request headers,
 // which in NGINX is done with the proxy_set_header directive.
 type HTTPRequestHeaderValidator struct{}
@@ -20,12 +30,6 @@ func (HTTPRedirectValidator) ValidateRedirectScheme(scheme string) (valid bool,
 	return validateInSupportedValues(scheme, supportedRedirectSchemes)
 }
 
-var redirectHostnameExamples = []string{"host", "example.com"}
-
-func (HTTPRedirectValidator) ValidateRedirectHostname(hostname string) error {
-	return validateEscapedStringNoVarExpansion(hostname, redirectHostnameExamples)
-}
-
 func (HTTPRedirectValidator) ValidateRedirectPort(_ int32) error {
 	// any value is allowed
 	return nil
@@ -44,6 +48,30 @@ func (HTTPRedirectValidator) ValidateRedirectStatusCode(statusCode int) (valid b
 	return validateInSupportedValues(statusCode, supportedRedirectStatusCodes)
 }
 
+var hostnameExamples = []string{"host", "example.com"}
+
+func (HTTPRedirectValidator) ValidateHostname(hostname string) error {
+	return validateEscapedStringNoVarExpansion(hostname, hostnameExamples)
+}
+
+// ValidateRewritePath validates a path used in a URL Rewrite filter.
+func (HTTPURLRewriteValidator) ValidateRewritePath(path string) error {
+	if path == "" {
+		return nil
+	}
+
+	if !pathRegexp.MatchString(path) {
+		msg := k8svalidation.RegexError(pathErrMsg, pathFmt, pathExamples...)
+		return errors.New(msg)
+	}
+
+	if strings.Contains(path, "$") {
+		return errors.New("cannot contain $")
+	}
+
+	return nil
+}
+
 func (HTTPRequestHeaderValidator) ValidateRequestHeaderName(name string) error {
 	return validateHeaderName(name)
 }
diff --git a/internal/mode/static/nginx/config/validation/http_filters_test.go b/internal/mode/static/nginx/config/validation/http_filters_test.go
index 9ebc269469..c216a30224 100644
--- a/internal/mode/static/nginx/config/validation/http_filters_test.go
+++ b/internal/mode/static/nginx/config/validation/http_filters_test.go
@@ -23,22 +23,6 @@ func TestValidateRedirectScheme(t *testing.T) {
 	)
 }
 
-func TestValidateRedirectHostname(t *testing.T) {
-	validator := HTTPRedirectValidator{}
-
-	testValidValuesForSimpleValidator(
-		t,
-		validator.ValidateRedirectHostname,
-		"example.com",
-	)
-
-	testInvalidValuesForSimpleValidator(
-		t,
-		validator.ValidateRedirectHostname,
-		"example.com$",
-	)
-}
-
 func TestValidateRedirectPort(t *testing.T) {
 	validator := HTTPRedirectValidator{}
 
@@ -67,6 +51,43 @@ func TestValidateRedirectStatusCode(t *testing.T) {
 	)
 }
 
+func TestValidateHostname(t *testing.T) {
+	validator := HTTPRedirectValidator{}
+
+	testValidValuesForSimpleValidator(
+		t,
+		validator.ValidateHostname,
+		"example.com",
+	)
+
+	testInvalidValuesForSimpleValidator(
+		t,
+		validator.ValidateHostname,
+		"example.com$",
+	)
+}
+
+func TestValidateRewritePath(t *testing.T) {
+	validator := HTTPURLRewriteValidator{}
+
+	testValidValuesForSimpleValidator(
+		t,
+		validator.ValidateRewritePath,
+		"",
+		"/path",
+		"/longer/path",
+		"/trailing/",
+	)
+
+	testInvalidValuesForSimpleValidator(
+		t,
+		validator.ValidateRewritePath,
+		"path",
+		"$path",
+		"/path$",
+	)
+}
+
 func TestValidateRequestHeaderName(t *testing.T) {
 	validator := HTTPRequestHeaderValidator{}
 
diff --git a/internal/mode/static/nginx/config/validation/http_njs_match.go b/internal/mode/static/nginx/config/validation/http_njs_match.go
index dc224c38e7..0db9ce6d9e 100644
--- a/internal/mode/static/nginx/config/validation/http_njs_match.go
+++ b/internal/mode/static/nginx/config/validation/http_njs_match.go
@@ -3,7 +3,6 @@ package validation
 import (
 	"errors"
 	"fmt"
-	"regexp"
 	"strings"
 
 	k8svalidation "k8s.io/apimachinery/pkg/util/validation"
@@ -16,16 +15,6 @@ import (
 // so changes to the implementation change the validation rules here.
 type HTTPNJSMatchValidator struct{}
 
-const (
-	pathFmt    = `/[^\s{};]*`
-	pathErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`"
-)
-
-var (
-	pathRegexp   = regexp.MustCompile("^" + pathFmt + "$")
-	pathExamples = []string{"/", "/path", "/path/subpath-123"}
-)
-
 // ValidatePathInMatch a path used in the location directive.
 func (HTTPNJSMatchValidator) ValidatePathInMatch(path string) error {
 	if path == "" {
diff --git a/internal/mode/static/nginx/config/validation/http_validator.go b/internal/mode/static/nginx/config/validation/http_validator.go
index 10c98aec34..f33adb8434 100644
--- a/internal/mode/static/nginx/config/validation/http_validator.go
+++ b/internal/mode/static/nginx/config/validation/http_validator.go
@@ -10,6 +10,7 @@ import (
 type HTTPValidator struct {
 	HTTPNJSMatchValidator
 	HTTPRedirectValidator
+	HTTPURLRewriteValidator
 	HTTPRequestHeaderValidator
 }
 
diff --git a/internal/mode/static/nginx/modules/src/httpmatches.js b/internal/mode/static/nginx/modules/src/httpmatches.js
index 436a0391fd..1f36ae57ec 100644
--- a/internal/mode/static/nginx/modules/src/httpmatches.js
+++ b/internal/mode/static/nginx/modules/src/httpmatches.js
@@ -1,3 +1,5 @@
+import qs from 'querystring';
+
 const MATCHES_VARIABLE = 'http_matches';
 const HTTP_CODES = {
   notFound: 404,
@@ -44,7 +46,14 @@ function redirect(r) {
     return;
   }
 
-  r.internalRedirect(match.redirectPath);
+  // If performing a rewrite, $request_uri won't be used,
+  // so we have to preserve args in the internal redirect.
+  let args = qs.stringify(r.args);
+  if (args) {
+    args = '?' + args;
+  }
+
+  r.internalRedirect(match.redirectPath + args);
 }
 
 function extractMatchesFromRequest(r) {
diff --git a/internal/mode/static/nginx/modules/test/httpmatches.test.js b/internal/mode/static/nginx/modules/test/httpmatches.test.js
index 669638b819..ac020abba9 100644
--- a/internal/mode/static/nginx/modules/test/httpmatches.test.js
+++ b/internal/mode/static/nginx/modules/test/httpmatches.test.js
@@ -439,7 +439,7 @@ describe('redirect', () => {
         params: { Arg1: 'value1', arg2: 'value2=SOME=other=value' },
       }),
       matches: [testHeaderMatches, testQueryParamMatches, testAllMatchTypes, testAnyMatch], // request matches testAllMatchTypes and testAnyMatch. But first match should win.
-      expectedRedirect: '/a-match',
+      expectedRedirect: '/a-match?Arg1=value1&arg2=value2%3DSOME%3Dother%3Dvalue',
     },
   ];
 
diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go
index dab0323bce..81ebb817c3 100644
--- a/internal/mode/static/state/dataplane/configuration.go
+++ b/internal/mode/static/state/dataplane/configuration.go
@@ -455,6 +455,11 @@ func createHTTPFilters(filters []v1.HTTPRouteFilter) HTTPFilters {
 				// using the first filter
 				result.RequestRedirect = convertHTTPRequestRedirectFilter(f.RequestRedirect)
 			}
+		case v1.HTTPRouteFilterURLRewrite:
+			if result.RequestURLRewrite == nil {
+				// using the first filter
+				result.RequestURLRewrite = convertHTTPURLRewriteFilter(f.URLRewrite)
+			}
 		case v1.HTTPRouteFilterRequestHeaderModifier:
 			if result.RequestHeaderModifiers == nil {
 				// using the first filter
diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go
index f23e9ec9f7..d4d8a3a7bf 100644
--- a/internal/mode/static/state/dataplane/configuration_test.go
+++ b/internal/mode/static/state/dataplane/configuration_test.go
@@ -1649,6 +1649,18 @@ func TestCreateFilters(t *testing.T) {
 			Hostname: helpers.GetPointer[v1.PreciseHostname]("bar.example.com"),
 		},
 	}
+	rewrite1 := v1.HTTPRouteFilter{
+		Type: v1.HTTPRouteFilterURLRewrite,
+		URLRewrite: &v1.HTTPURLRewriteFilter{
+			Hostname: helpers.GetPointer[v1.PreciseHostname]("foo.example.com"),
+		},
+	}
+	rewrite2 := v1.HTTPRouteFilter{
+		Type: v1.HTTPRouteFilterURLRewrite,
+		URLRewrite: &v1.HTTPURLRewriteFilter{
+			Hostname: helpers.GetPointer[v1.PreciseHostname]("bar.example.com"),
+		},
+	}
 	requestHeaderModifiers1 := v1.HTTPRouteFilter{
 		Type: v1.HTTPRouteFilterRequestHeaderModifier,
 		RequestHeaderModifier: &v1.HTTPHeaderFilter{
@@ -1675,6 +1687,9 @@ func TestCreateFilters(t *testing.T) {
 	expectedRedirect1 := HTTPRequestRedirectFilter{
 		Hostname: helpers.GetPointer("foo.example.com"),
 	}
+	expectedRewrite1 := HTTPURLRewriteFilter{
+		Hostname: helpers.GetPointer("foo.example.com"),
+	}
 	expectedHeaderModifier1 := HTTPHeaderFilter{
 		Set: []HTTPHeader{
 			{
@@ -1729,14 +1744,17 @@ func TestCreateFilters(t *testing.T) {
 			filters: []v1.HTTPRouteFilter{
 				redirect1,
 				redirect2,
+				rewrite1,
+				rewrite2,
 				requestHeaderModifiers1,
 				requestHeaderModifiers2,
 			},
 			expected: HTTPFilters{
 				RequestRedirect:        &expectedRedirect1,
+				RequestURLRewrite:      &expectedRewrite1,
 				RequestHeaderModifiers: &expectedHeaderModifier1,
 			},
-			msg: "two redirect filters, two request header modifier, first value for each wins",
+			msg: "two of each filter, first value for each wins",
 		},
 	}
 
diff --git a/internal/mode/static/state/dataplane/convert.go b/internal/mode/static/state/dataplane/convert.go
index 2ca8713e85..4337a0d787 100644
--- a/internal/mode/static/state/dataplane/convert.go
+++ b/internal/mode/static/state/dataplane/convert.go
@@ -46,6 +46,13 @@ func convertHTTPRequestRedirectFilter(filter *v1.HTTPRequestRedirectFilter) *HTT
 	}
 }
 
+func convertHTTPURLRewriteFilter(filter *v1.HTTPURLRewriteFilter) *HTTPURLRewriteFilter {
+	return &HTTPURLRewriteFilter{
+		Hostname: (*string)(filter.Hostname),
+		Path:     convertPathModifier(filter.Path),
+	}
+}
+
 func convertHTTPHeaderFilter(filter *v1.HTTPHeaderFilter) *HTTPHeaderFilter {
 	result := &HTTPHeaderFilter{
 		Remove: filter.Remove,
@@ -78,3 +85,22 @@ func convertPathType(pathType v1.PathMatchType) PathType {
 		panic(fmt.Sprintf("unsupported path type: %s", pathType))
 	}
 }
+
+func convertPathModifier(path *v1.HTTPPathModifier) *HTTPPathModifier {
+	if path != nil {
+		switch path.Type {
+		case v1.FullPathHTTPPathModifier:
+			return &HTTPPathModifier{
+				Type:        ReplaceFullPath,
+				Replacement: *path.ReplaceFullPath,
+			}
+		case v1.PrefixMatchHTTPPathModifier:
+			return &HTTPPathModifier{
+				Type:        ReplacePrefixMatch,
+				Replacement: *path.ReplacePrefixMatch,
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/internal/mode/static/state/dataplane/convert_test.go b/internal/mode/static/state/dataplane/convert_test.go
index 27feeec417..1688c1591f 100644
--- a/internal/mode/static/state/dataplane/convert_test.go
+++ b/internal/mode/static/state/dataplane/convert_test.go
@@ -161,6 +161,63 @@ func TestConvertHTTPRequestRedirectFilter(t *testing.T) {
 	}
 }
 
+func TestConvertHTTPURLRewriteFilter(t *testing.T) {
+	tests := []struct {
+		filter   *v1.HTTPURLRewriteFilter
+		expected *HTTPURLRewriteFilter
+		name     string
+	}{
+		{
+			filter:   &v1.HTTPURLRewriteFilter{},
+			expected: &HTTPURLRewriteFilter{},
+			name:     "empty",
+		},
+		{
+			filter: &v1.HTTPURLRewriteFilter{
+				Hostname: helpers.GetPointer[v1.PreciseHostname]("example.com"),
+				Path: &v1.HTTPPathModifier{
+					Type:            v1.FullPathHTTPPathModifier,
+					ReplaceFullPath: helpers.GetPointer("/path"),
+				},
+			},
+			expected: &HTTPURLRewriteFilter{
+				Hostname: helpers.GetPointer("example.com"),
+				Path: &HTTPPathModifier{
+					Type:        ReplaceFullPath,
+					Replacement: "/path",
+				},
+			},
+			name: "full path modifier",
+		},
+		{
+			filter: &v1.HTTPURLRewriteFilter{
+				Hostname: helpers.GetPointer[v1.PreciseHostname]("example.com"),
+				Path: &v1.HTTPPathModifier{
+					Type:               v1.PrefixMatchHTTPPathModifier,
+					ReplacePrefixMatch: helpers.GetPointer("/path"),
+				},
+			},
+			expected: &HTTPURLRewriteFilter{
+				Hostname: helpers.GetPointer("example.com"),
+				Path: &HTTPPathModifier{
+					Type:        ReplacePrefixMatch,
+					Replacement: "/path",
+				},
+			},
+			name: "prefix path modifier",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			result := convertHTTPURLRewriteFilter(test.filter)
+			g.Expect(result).To(Equal(test.expected))
+		})
+	}
+}
+
 func TestConvertHTTPHeaderFilter(t *testing.T) {
 	tests := []struct {
 		filter   *v1.HTTPHeaderFilter
diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go
index 98d61c73f5..465e421ea3 100644
--- a/internal/mode/static/state/dataplane/types.go
+++ b/internal/mode/static/state/dataplane/types.go
@@ -97,6 +97,8 @@ type HTTPFilters struct {
 	InvalidFilter *InvalidHTTPFilter
 	// RequestRedirect holds the HTTPRequestRedirectFilter.
 	RequestRedirect *HTTPRequestRedirectFilter
+	// RequestURLRewrite holds the HTTPURLRewriteFilter.
+	RequestURLRewrite *HTTPURLRewriteFilter
 	// RequestHeaderModifiers holds the HTTPHeaderFilter.
 	RequestHeaderModifiers *HTTPHeaderFilter
 }
@@ -131,6 +133,33 @@ type HTTPRequestRedirectFilter struct {
 	StatusCode *int
 }
 
+// HTTPURLRewriteFilter rewrites HTTP requests.
+type HTTPURLRewriteFilter struct {
+	// Hostname is the hostname of the rewrite.
+	Hostname *string
+	// Path is the path of the rewrite.
+	Path *HTTPPathModifier
+}
+
+// PathModifierType is the type of the PathModifier in a redirect or rewrite rule.
+type PathModifierType string
+
+const (
+	// ReplaceFullPath indicates that we replace the full path.
+	ReplaceFullPath PathModifierType = "ReplaceFullPath"
+	// ReplacePrefixMatch indicates that we replace a prefix match.
+	ReplacePrefixMatch PathModifierType = "ReplacePrefixMatch"
+)
+
+// HTTPPathModifier defines configuration for path modifiers.
+type HTTPPathModifier struct {
+	// Replacement specifies the value with which to replace the full path or prefix match of a request during
+	// a rewrite or redirect.
+	Replacement string
+	// Type indicates the type of path modifier.
+	Type PathModifierType
+}
+
 // HTTPHeaderMatch matches an HTTP header.
 type HTTPHeaderMatch struct {
 	// Name is the name of the header to match.
diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go
index 65079a0c83..c8f42997e2 100644
--- a/internal/mode/static/state/graph/httproute.go
+++ b/internal/mode/static/state/graph/httproute.go
@@ -693,6 +693,8 @@ func validateFilter(
 	switch filter.Type {
 	case v1.HTTPRouteFilterRequestRedirect:
 		return validateFilterRedirect(validator, filter, filterPath)
+	case v1.HTTPRouteFilterURLRewrite:
+		return validateFilterRewrite(validator, filter, filterPath)
 	case v1.HTTPRouteFilterRequestHeaderModifier:
 		return validateFilterHeaderModifier(validator, filter, filterPath)
 	default:
@@ -701,6 +703,7 @@ func validateFilter(
 			filter.Type,
 			[]string{
 				string(v1.HTTPRouteFilterRequestRedirect),
+				string(v1.HTTPRouteFilterURLRewrite),
 				string(v1.HTTPRouteFilterRequestHeaderModifier),
 			},
 		)
@@ -721,7 +724,6 @@ func validateFilterRedirect(
 	}
 
 	redirect := filter.RequestRedirect
-
 	redirectPath := filterPath.Child("requestRedirect")
 
 	if redirect.Scheme != nil {
@@ -732,7 +734,7 @@ func validateFilterRedirect(
 	}
 
 	if redirect.Hostname != nil {
-		if err := validator.ValidateRedirectHostname(string(*redirect.Hostname)); err != nil {
+		if err := validator.ValidateHostname(string(*redirect.Hostname)); err != nil {
 			valErr := field.Invalid(redirectPath.Child("hostname"), *redirect.Hostname, err.Error())
 			allErrs = append(allErrs, valErr)
 		}
@@ -760,6 +762,49 @@ func validateFilterRedirect(
 	return allErrs
 }
 
+func validateFilterRewrite(
+	validator validation.HTTPFieldsValidator,
+	filter v1.HTTPRouteFilter,
+	filterPath *field.Path,
+) field.ErrorList {
+	var allErrs field.ErrorList
+
+	if filter.URLRewrite == nil {
+		panicForBrokenWebhookAssumption(errors.New("urlRewrite cannot be nil"))
+	}
+
+	rewrite := filter.URLRewrite
+	rewritePath := filterPath.Child("urlRewrite")
+
+	if rewrite.Hostname != nil {
+		if err := validator.ValidateHostname(string(*rewrite.Hostname)); err != nil {
+			valErr := field.Invalid(rewritePath.Child("hostname"), *rewrite.Hostname, err.Error())
+			allErrs = append(allErrs, valErr)
+		}
+	}
+
+	if rewrite.Path != nil {
+		var path string
+		switch rewrite.Path.Type {
+		case v1.FullPathHTTPPathModifier:
+			path = *rewrite.Path.ReplaceFullPath
+		case v1.PrefixMatchHTTPPathModifier:
+			path = *rewrite.Path.ReplacePrefixMatch
+		default:
+			msg := fmt.Sprintf("urlRewrite path type %s not supported", rewrite.Path.Type)
+			valErr := field.Invalid(rewritePath.Child("path"), *rewrite.Path, msg)
+			return append(allErrs, valErr)
+		}
+
+		if err := validator.ValidateRewritePath(path); err != nil {
+			valErr := field.Invalid(rewritePath.Child("path"), *rewrite.Path, err.Error())
+			allErrs = append(allErrs, valErr)
+		}
+	}
+
+	return allErrs
+}
+
 func validateFilterHeaderModifier(
 	validator validation.HTTPFieldsValidator,
 	filter v1.HTTPRouteFilter,
diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go
index c8d7c4d7e0..ec3b5ea521 100644
--- a/internal/mode/static/state/graph/httproute_test.go
+++ b/internal/mode/static/state/graph/httproute_test.go
@@ -375,7 +375,7 @@ func TestBuildRoute(t *testing.T) {
 			}
 			return nil
 		},
-		ValidateRedirectHostnameStub: func(h string) error {
+		ValidateHostnameStub: func(h string) error {
 			if h == invalidRedirectHostname {
 				return errors.New("invalid hostname")
 			}
@@ -1856,6 +1856,14 @@ func TestValidateFilter(t *testing.T) {
 			expectErrCount: 0,
 			name:           "valid redirect filter",
 		},
+		{
+			filter: gatewayv1.HTTPRouteFilter{
+				Type:       gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{},
+			},
+			expectErrCount: 0,
+			name:           "valid rewrite filter",
+		},
 		{
 			filter: gatewayv1.HTTPRouteFilter{
 				Type:                  gatewayv1.HTTPRouteFilterRequestHeaderModifier,
@@ -1866,7 +1874,7 @@ func TestValidateFilter(t *testing.T) {
 		},
 		{
 			filter: gatewayv1.HTTPRouteFilter{
-				Type: gatewayv1.HTTPRouteFilterURLRewrite,
+				Type: gatewayv1.HTTPRouteFilterRequestMirror,
 			},
 			expectErrCount: 1,
 			name:           "unsupported filter",
@@ -1899,7 +1907,17 @@ func TestValidateFilterRedirect(t *testing.T) {
 		validator      *validationfakes.FakeHTTPFieldsValidator
 		name           string
 		expectErrCount int
+		panic          bool
 	}{
+		{
+			validator: &validationfakes.FakeHTTPFieldsValidator{},
+			filter: gatewayv1.HTTPRouteFilter{
+				Type:            gatewayv1.HTTPRouteFilterRequestRedirect,
+				RequestRedirect: nil,
+			},
+			panic: true,
+			name:  "nil filter",
+		},
 		{
 			validator: createAllValidValidator(),
 			filter: gatewayv1.HTTPRouteFilter{
@@ -1941,7 +1959,7 @@ func TestValidateFilterRedirect(t *testing.T) {
 		{
 			validator: func() *validationfakes.FakeHTTPFieldsValidator {
 				validator := createAllValidValidator()
-				validator.ValidateRedirectHostnameReturns(errors.New("invalid hostname"))
+				validator.ValidateHostnameReturns(errors.New("invalid hostname"))
 				return validator
 			}(),
 			filter: gatewayv1.HTTPRouteFilter{
@@ -1999,7 +2017,7 @@ func TestValidateFilterRedirect(t *testing.T) {
 		{
 			validator: func() *validationfakes.FakeHTTPFieldsValidator {
 				validator := createAllValidValidator()
-				validator.ValidateRedirectHostnameReturns(errors.New("invalid hostname"))
+				validator.ValidateHostnameReturns(errors.New("invalid hostname"))
 				validator.ValidateRedirectPortReturns(errors.New("invalid port"))
 				return validator
 			}(),
@@ -2024,8 +2042,164 @@ func TestValidateFilterRedirect(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			g := NewWithT(t)
-			allErrs := validateFilterRedirect(test.validator, test.filter, filterPath)
-			g.Expect(allErrs).To(HaveLen(test.expectErrCount))
+			if test.panic {
+				validate := func() {
+					_ = validateFilterRedirect(test.validator, test.filter, filterPath)
+				}
+				g.Expect(validate).To(Panic())
+			} else {
+				allErrs := validateFilterRedirect(test.validator, test.filter, filterPath)
+				g.Expect(allErrs).To(HaveLen(test.expectErrCount))
+			}
+		})
+	}
+}
+
+func TestValidateFilterRewrite(t *testing.T) {
+	tests := []struct {
+		filter         gatewayv1.HTTPRouteFilter
+		validator      *validationfakes.FakeHTTPFieldsValidator
+		name           string
+		expectErrCount int
+		panic          bool
+	}{
+		{
+			validator: &validationfakes.FakeHTTPFieldsValidator{},
+			filter: gatewayv1.HTTPRouteFilter{
+				Type:       gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: nil,
+			},
+			panic: true,
+			name:  "nil filter",
+		},
+		{
+			validator: &validationfakes.FakeHTTPFieldsValidator{},
+			filter: gatewayv1.HTTPRouteFilter{
+				Type: gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+					Hostname: helpers.GetPointer[gatewayv1.PreciseHostname]("example.com"),
+					Path: &gatewayv1.HTTPPathModifier{
+						Type:            gatewayv1.FullPathHTTPPathModifier,
+						ReplaceFullPath: helpers.GetPointer("/path"),
+					},
+				},
+			},
+			expectErrCount: 0,
+			name:           "valid rewrite filter",
+		},
+		{
+			validator: &validationfakes.FakeHTTPFieldsValidator{},
+			filter: gatewayv1.HTTPRouteFilter{
+				Type:       gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{},
+			},
+			expectErrCount: 0,
+			name:           "valid rewrite filter with no fields set",
+		},
+		{
+			validator: func() *validationfakes.FakeHTTPFieldsValidator {
+				validator := &validationfakes.FakeHTTPFieldsValidator{}
+				validator.ValidateHostnameReturns(errors.New("invalid hostname"))
+				return validator
+			}(),
+			filter: gatewayv1.HTTPRouteFilter{
+				Type: gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+					Hostname: helpers.GetPointer[gatewayv1.PreciseHostname](
+						"example.com",
+					), // any value is invalid by the validator
+				},
+			},
+			expectErrCount: 1,
+			name:           "rewrite filter with invalid hostname",
+		},
+		{
+			validator: &validationfakes.FakeHTTPFieldsValidator{},
+			filter: gatewayv1.HTTPRouteFilter{
+				Type: gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+					Path: &gatewayv1.HTTPPathModifier{
+						Type: "bad-type",
+					},
+				},
+			},
+			expectErrCount: 1,
+			name:           "rewrite filter with invalid path type",
+		},
+		{
+			validator: func() *validationfakes.FakeHTTPFieldsValidator {
+				validator := &validationfakes.FakeHTTPFieldsValidator{}
+				validator.ValidateRewritePathReturns(errors.New("invalid path value"))
+				return validator
+			}(),
+			filter: gatewayv1.HTTPRouteFilter{
+				Type: gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+					Path: &gatewayv1.HTTPPathModifier{
+						Type:            gatewayv1.FullPathHTTPPathModifier,
+						ReplaceFullPath: helpers.GetPointer("/path"),
+					}, // any value is invalid by the validator
+				},
+			},
+			expectErrCount: 1,
+			name:           "rewrite filter with invalid full path",
+		},
+		{
+			validator: func() *validationfakes.FakeHTTPFieldsValidator {
+				validator := &validationfakes.FakeHTTPFieldsValidator{}
+				validator.ValidateRewritePathReturns(errors.New("invalid path"))
+				return validator
+			}(),
+			filter: gatewayv1.HTTPRouteFilter{
+				Type: gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+					Path: &gatewayv1.HTTPPathModifier{
+						Type:               gatewayv1.PrefixMatchHTTPPathModifier,
+						ReplacePrefixMatch: helpers.GetPointer("/path"),
+					}, // any value is invalid by the validator
+				},
+			},
+			expectErrCount: 1,
+			name:           "rewrite filter with invalid prefix path",
+		},
+		{
+			validator: func() *validationfakes.FakeHTTPFieldsValidator {
+				validator := &validationfakes.FakeHTTPFieldsValidator{}
+				validator.ValidateHostnameReturns(errors.New("invalid hostname"))
+				validator.ValidateRewritePathReturns(errors.New("invalid path"))
+				return validator
+			}(),
+			filter: gatewayv1.HTTPRouteFilter{
+				Type: gatewayv1.HTTPRouteFilterURLRewrite,
+				URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+					Hostname: helpers.GetPointer[gatewayv1.PreciseHostname](
+						"example.com",
+					), // any value is invalid by the validator
+					Path: &gatewayv1.HTTPPathModifier{
+						Type:               gatewayv1.PrefixMatchHTTPPathModifier,
+						ReplacePrefixMatch: helpers.GetPointer("/path"),
+					}, // any value is invalid by the validator
+				},
+			},
+			expectErrCount: 2,
+			name:           "rewrite filter with multiple errors",
+		},
+	}
+
+	filterPath := field.NewPath("test")
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+			if test.panic {
+				validate := func() {
+					_ = validateFilterRewrite(test.validator, test.filter, filterPath)
+				}
+				g.Expect(validate).To(Panic())
+			} else {
+				allErrs := validateFilterRewrite(test.validator, test.filter, filterPath)
+				g.Expect(allErrs).To(HaveLen(test.expectErrCount))
+			}
 		})
 	}
 }
diff --git a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go
index 4a82f1c08c..05b4c620b6 100644
--- a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go
+++ b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go
@@ -30,6 +30,17 @@ type FakeHTTPFieldsValidator struct {
 	validateHeaderValueInMatchReturnsOnCall map[int]struct {
 		result1 error
 	}
+	ValidateHostnameStub        func(string) error
+	validateHostnameMutex       sync.RWMutex
+	validateHostnameArgsForCall []struct {
+		arg1 string
+	}
+	validateHostnameReturns struct {
+		result1 error
+	}
+	validateHostnameReturnsOnCall map[int]struct {
+		result1 error
+	}
 	ValidateMethodInMatchStub        func(string) (bool, []string)
 	validateMethodInMatchMutex       sync.RWMutex
 	validateMethodInMatchArgsForCall []struct {
@@ -76,17 +87,6 @@ type FakeHTTPFieldsValidator struct {
 	validateQueryParamValueInMatchReturnsOnCall map[int]struct {
 		result1 error
 	}
-	ValidateRedirectHostnameStub        func(string) error
-	validateRedirectHostnameMutex       sync.RWMutex
-	validateRedirectHostnameArgsForCall []struct {
-		arg1 string
-	}
-	validateRedirectHostnameReturns struct {
-		result1 error
-	}
-	validateRedirectHostnameReturnsOnCall map[int]struct {
-		result1 error
-	}
 	ValidateRedirectPortStub        func(int32) error
 	validateRedirectPortMutex       sync.RWMutex
 	validateRedirectPortArgsForCall []struct {
@@ -146,6 +146,17 @@ type FakeHTTPFieldsValidator struct {
 	validateRequestHeaderValueReturnsOnCall map[int]struct {
 		result1 error
 	}
+	ValidateRewritePathStub        func(string) error
+	validateRewritePathMutex       sync.RWMutex
+	validateRewritePathArgsForCall []struct {
+		arg1 string
+	}
+	validateRewritePathReturns struct {
+		result1 error
+	}
+	validateRewritePathReturnsOnCall map[int]struct {
+		result1 error
+	}
 	invocations      map[string][][]interface{}
 	invocationsMutex sync.RWMutex
 }
@@ -272,6 +283,67 @@ func (fake *FakeHTTPFieldsValidator) ValidateHeaderValueInMatchReturnsOnCall(i i
 	}{result1}
 }
 
+func (fake *FakeHTTPFieldsValidator) ValidateHostname(arg1 string) error {
+	fake.validateHostnameMutex.Lock()
+	ret, specificReturn := fake.validateHostnameReturnsOnCall[len(fake.validateHostnameArgsForCall)]
+	fake.validateHostnameArgsForCall = append(fake.validateHostnameArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.ValidateHostnameStub
+	fakeReturns := fake.validateHostnameReturns
+	fake.recordInvocation("ValidateHostname", []interface{}{arg1})
+	fake.validateHostnameMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateHostnameCallCount() int {
+	fake.validateHostnameMutex.RLock()
+	defer fake.validateHostnameMutex.RUnlock()
+	return len(fake.validateHostnameArgsForCall)
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateHostnameCalls(stub func(string) error) {
+	fake.validateHostnameMutex.Lock()
+	defer fake.validateHostnameMutex.Unlock()
+	fake.ValidateHostnameStub = stub
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateHostnameArgsForCall(i int) string {
+	fake.validateHostnameMutex.RLock()
+	defer fake.validateHostnameMutex.RUnlock()
+	argsForCall := fake.validateHostnameArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateHostnameReturns(result1 error) {
+	fake.validateHostnameMutex.Lock()
+	defer fake.validateHostnameMutex.Unlock()
+	fake.ValidateHostnameStub = nil
+	fake.validateHostnameReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateHostnameReturnsOnCall(i int, result1 error) {
+	fake.validateHostnameMutex.Lock()
+	defer fake.validateHostnameMutex.Unlock()
+	fake.ValidateHostnameStub = nil
+	if fake.validateHostnameReturnsOnCall == nil {
+		fake.validateHostnameReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.validateHostnameReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
 func (fake *FakeHTTPFieldsValidator) ValidateMethodInMatch(arg1 string) (bool, []string) {
 	fake.validateMethodInMatchMutex.Lock()
 	ret, specificReturn := fake.validateMethodInMatchReturnsOnCall[len(fake.validateMethodInMatchArgsForCall)]
@@ -519,67 +591,6 @@ func (fake *FakeHTTPFieldsValidator) ValidateQueryParamValueInMatchReturnsOnCall
 	}{result1}
 }
 
-func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostname(arg1 string) error {
-	fake.validateRedirectHostnameMutex.Lock()
-	ret, specificReturn := fake.validateRedirectHostnameReturnsOnCall[len(fake.validateRedirectHostnameArgsForCall)]
-	fake.validateRedirectHostnameArgsForCall = append(fake.validateRedirectHostnameArgsForCall, struct {
-		arg1 string
-	}{arg1})
-	stub := fake.ValidateRedirectHostnameStub
-	fakeReturns := fake.validateRedirectHostnameReturns
-	fake.recordInvocation("ValidateRedirectHostname", []interface{}{arg1})
-	fake.validateRedirectHostnameMutex.Unlock()
-	if stub != nil {
-		return stub(arg1)
-	}
-	if specificReturn {
-		return ret.result1
-	}
-	return fakeReturns.result1
-}
-
-func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameCallCount() int {
-	fake.validateRedirectHostnameMutex.RLock()
-	defer fake.validateRedirectHostnameMutex.RUnlock()
-	return len(fake.validateRedirectHostnameArgsForCall)
-}
-
-func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameCalls(stub func(string) error) {
-	fake.validateRedirectHostnameMutex.Lock()
-	defer fake.validateRedirectHostnameMutex.Unlock()
-	fake.ValidateRedirectHostnameStub = stub
-}
-
-func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameArgsForCall(i int) string {
-	fake.validateRedirectHostnameMutex.RLock()
-	defer fake.validateRedirectHostnameMutex.RUnlock()
-	argsForCall := fake.validateRedirectHostnameArgsForCall[i]
-	return argsForCall.arg1
-}
-
-func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameReturns(result1 error) {
-	fake.validateRedirectHostnameMutex.Lock()
-	defer fake.validateRedirectHostnameMutex.Unlock()
-	fake.ValidateRedirectHostnameStub = nil
-	fake.validateRedirectHostnameReturns = struct {
-		result1 error
-	}{result1}
-}
-
-func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameReturnsOnCall(i int, result1 error) {
-	fake.validateRedirectHostnameMutex.Lock()
-	defer fake.validateRedirectHostnameMutex.Unlock()
-	fake.ValidateRedirectHostnameStub = nil
-	if fake.validateRedirectHostnameReturnsOnCall == nil {
-		fake.validateRedirectHostnameReturnsOnCall = make(map[int]struct {
-			result1 error
-		})
-	}
-	fake.validateRedirectHostnameReturnsOnCall[i] = struct {
-		result1 error
-	}{result1}
-}
-
 func (fake *FakeHTTPFieldsValidator) ValidateRedirectPort(arg1 int32) error {
 	fake.validateRedirectPortMutex.Lock()
 	ret, specificReturn := fake.validateRedirectPortReturnsOnCall[len(fake.validateRedirectPortArgsForCall)]
@@ -891,6 +902,67 @@ func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValueReturnsOnCall(i i
 	}{result1}
 }
 
+func (fake *FakeHTTPFieldsValidator) ValidateRewritePath(arg1 string) error {
+	fake.validateRewritePathMutex.Lock()
+	ret, specificReturn := fake.validateRewritePathReturnsOnCall[len(fake.validateRewritePathArgsForCall)]
+	fake.validateRewritePathArgsForCall = append(fake.validateRewritePathArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.ValidateRewritePathStub
+	fakeReturns := fake.validateRewritePathReturns
+	fake.recordInvocation("ValidateRewritePath", []interface{}{arg1})
+	fake.validateRewritePathMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateRewritePathCallCount() int {
+	fake.validateRewritePathMutex.RLock()
+	defer fake.validateRewritePathMutex.RUnlock()
+	return len(fake.validateRewritePathArgsForCall)
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateRewritePathCalls(stub func(string) error) {
+	fake.validateRewritePathMutex.Lock()
+	defer fake.validateRewritePathMutex.Unlock()
+	fake.ValidateRewritePathStub = stub
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateRewritePathArgsForCall(i int) string {
+	fake.validateRewritePathMutex.RLock()
+	defer fake.validateRewritePathMutex.RUnlock()
+	argsForCall := fake.validateRewritePathArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateRewritePathReturns(result1 error) {
+	fake.validateRewritePathMutex.Lock()
+	defer fake.validateRewritePathMutex.Unlock()
+	fake.ValidateRewritePathStub = nil
+	fake.validateRewritePathReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeHTTPFieldsValidator) ValidateRewritePathReturnsOnCall(i int, result1 error) {
+	fake.validateRewritePathMutex.Lock()
+	defer fake.validateRewritePathMutex.Unlock()
+	fake.ValidateRewritePathStub = nil
+	if fake.validateRewritePathReturnsOnCall == nil {
+		fake.validateRewritePathReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.validateRewritePathReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
 func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} {
 	fake.invocationsMutex.RLock()
 	defer fake.invocationsMutex.RUnlock()
@@ -898,6 +970,8 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} {
 	defer fake.validateHeaderNameInMatchMutex.RUnlock()
 	fake.validateHeaderValueInMatchMutex.RLock()
 	defer fake.validateHeaderValueInMatchMutex.RUnlock()
+	fake.validateHostnameMutex.RLock()
+	defer fake.validateHostnameMutex.RUnlock()
 	fake.validateMethodInMatchMutex.RLock()
 	defer fake.validateMethodInMatchMutex.RUnlock()
 	fake.validatePathInMatchMutex.RLock()
@@ -906,8 +980,6 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} {
 	defer fake.validateQueryParamNameInMatchMutex.RUnlock()
 	fake.validateQueryParamValueInMatchMutex.RLock()
 	defer fake.validateQueryParamValueInMatchMutex.RUnlock()
-	fake.validateRedirectHostnameMutex.RLock()
-	defer fake.validateRedirectHostnameMutex.RUnlock()
 	fake.validateRedirectPortMutex.RLock()
 	defer fake.validateRedirectPortMutex.RUnlock()
 	fake.validateRedirectSchemeMutex.RLock()
@@ -918,6 +990,8 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} {
 	defer fake.validateRequestHeaderNameMutex.RUnlock()
 	fake.validateRequestHeaderValueMutex.RLock()
 	defer fake.validateRequestHeaderValueMutex.RUnlock()
+	fake.validateRewritePathMutex.RLock()
+	defer fake.validateRewritePathMutex.RUnlock()
 	copiedInvocations := map[string][][]interface{}{}
 	for key, value := range fake.invocations {
 		copiedInvocations[key] = value
diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go
index d1ff623853..d6433ad363 100644
--- a/internal/mode/static/state/validation/validator.go
+++ b/internal/mode/static/state/validation/validator.go
@@ -20,9 +20,10 @@ type HTTPFieldsValidator interface {
 	ValidateQueryParamValueInMatch(name string) error
 	ValidateMethodInMatch(method string) (valid bool, supportedValues []string)
 	ValidateRedirectScheme(scheme string) (valid bool, supportedValues []string)
-	ValidateRedirectHostname(hostname string) error
 	ValidateRedirectPort(port int32) error
 	ValidateRedirectStatusCode(statusCode int) (valid bool, supportedValues []string)
+	ValidateHostname(hostname string) error
+	ValidateRewritePath(path string) error
 	ValidateRequestHeaderName(name string) error
 	ValidateRequestHeaderValue(value string) error
 }

From 2bb1509529b363fe2cc0ba1ac6076179d764b1ad Mon Sep 17 00:00:00 2001
From: Saylor Berman <s.berman@f5.com>
Date: Wed, 20 Dec 2023 08:15:12 -0700
Subject: [PATCH 2/2] How-to guides

---
 .../traffic-management/https-termination.md   | 245 ++++++++++++++++++
 .../redirects-and-rewrites.md                 | 203 +++++++++++++++
 .../routing-traffic-to-your-app.md            |   6 +-
 .../overview/gateway-api-compatibility.md     |   5 +-
 4 files changed, 453 insertions(+), 6 deletions(-)
 create mode 100644 site/content/how-to/traffic-management/https-termination.md
 create mode 100644 site/content/how-to/traffic-management/redirects-and-rewrites.md

diff --git a/site/content/how-to/traffic-management/https-termination.md b/site/content/how-to/traffic-management/https-termination.md
new file mode 100644
index 0000000000..1ca50f025f
--- /dev/null
+++ b/site/content/how-to/traffic-management/https-termination.md
@@ -0,0 +1,245 @@
+---
+title: "HTTPS Termination"
+description: "Learn how to terminate HTTPS traffic using NGINX Gateway Fabric."
+weight: 500
+toc: true
+docs: "DOCS-000"
+---
+
+In this guide, we will show how to configure HTTPS termination for your application, using an [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) redirect filter, secret, and [ReferenceGrant](https://gateway-api.sigs.k8s.io/api-types/referencegrant/).
+
+## Prerequisites
+
+- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric.
+- [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP address and port of NGINX Gateway Fabric into shell variables:
+
+   ```text
+   GW_IP=XXX.YYY.ZZZ.III
+   GW_PORT=<port number>
+   ```
+
+   Save the ports of NGINX Gateway Fabric:
+
+   ```text
+   GW_HTTP_PORT=<http port number>
+   GW_HTTPS_PORT=<https port number>
+   ```
+
+{{< note >}}In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.{{< /note >}}
+
+## Set up
+
+Create the **coffee** application in Kubernetes by copying and pasting the following block into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coffee
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: coffee
+  template:
+    metadata:
+      labels:
+        app: coffee
+    spec:
+      containers:
+      - name: coffee
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coffee
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: coffee
+EOF
+```
+
+This will create the **coffee** service and a deployment. Run the following command to verify the resources were created:
+
+```shell
+kubectl get pods,svc
+```
+
+Your output should include the **coffee** pod and the **coffee** service:
+
+```text
+NAME                          READY   STATUS      RESTARTS   AGE
+pod/coffee-6b8b6d6486-7fc78   1/1     Running   0          40s
+
+
+NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
+service/coffee       ClusterIP   10.96.189.37   <none>        80/TCP    40s
+```
+
+## Configure HTTPS Termination and Routing
+
+For the HTTPS, we need a certificate and key that are stored in a secret. This secret will live in a separate namespace, so we will need a ReferenceGrant in order to access it.
+
+To create the **certificate** namespace and secret, copy and paste the following into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: certificate
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: cafe-secret
+  namespace: certificate
+type: kubernetes.io/tls
+data:
+  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNzakNDQVpvQ0NRQzdCdVdXdWRtRkNEQU5CZ2txaGtpRzl3MEJBUXNGQURBYk1Sa3dGd1lEVlFRRERCQmoKWVdabExtVjRZVzF3YkdVdVkyOXRNQjRYRFRJeU1EY3hOREl4TlRJek9Wb1hEVEl6TURjeE5ESXhOVEl6T1ZvdwpHekVaTUJjR0ExVUVBd3dRWTJGbVpTNWxlR0Z0Y0d4bExtTnZiVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnRVBBRENDQVFvQ2dnRUJBTHFZMnRHNFc5aStFYzJhdnV4Q2prb2tnUUx1ek10U1Rnc1RNaEhuK3ZRUmxIam8KVzFLRnMvQVdlS25UUStyTWVKVWNseis4M3QwRGtyRThwUisxR2NKSE50WlNMb0NEYUlRN0Nhck5nY1daS0o4Qgo1WDNnVS9YeVJHZjI2c1REd2xzU3NkSEQ1U2U3K2Vab3NPcTdHTVF3K25HR2NVZ0VtL1Q1UEMvY05PWE0zZWxGClRPL051MStoMzROVG9BbDNQdTF2QlpMcDNQVERtQ0thaEROV0NWbUJQUWpNNFI4VERsbFhhMHQ5Z1o1MTRSRzUKWHlZWTNtdzZpUzIrR1dYVXllMjFuWVV4UEhZbDV4RHY0c0FXaGRXbElweHlZQlNCRURjczN6QlI2bFF1OWkxZAp0R1k4dGJ3blVmcUVUR3NZdWxzc05qcU95V1VEcFdJelhibHhJZVVDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBUUVBcjkrZWJ0U1dzSnhLTGtLZlRkek1ISFhOd2Y5ZXFVbHNtTXZmMGdBdWVKTUpUR215dG1iWjlpbXQKL2RnWlpYVE9hTElHUG9oZ3BpS0l5eVVRZVdGQ2F0NHRxWkNPVWRhbUloOGk0Q1h6QVJYVHNvcUNOenNNLzZMRQphM25XbFZyS2lmZHYrWkxyRi8vblc0VVNvOEoxaCtQeDljY0tpRDZZU0RVUERDRGh1RUtFWXcvbHpoUDJVOXNmCnl6cEJKVGQ4enFyM3paTjNGWWlITmgzYlRhQS82di9jU2lyamNTK1EwQXg4RWpzQzYxRjRVMTc4QzdWNWRCKzQKcmtPTy9QNlA0UFlWNTRZZHMvRjE2WkZJTHFBNENCYnExRExuYWRxamxyN3NPbzl2ZzNnWFNMYXBVVkdtZ2todAp6VlZPWG1mU0Z4OS90MDBHUi95bUdPbERJbWlXMGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
+  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzZtTnJSdUZ2WXZoSE4KbXI3c1FvNUtKSUVDN3N6TFVrNExFeklSNS9yMEVaUjQ2RnRTaGJQd0ZuaXAwMFBxekhpVkhKYy92TjdkQTVLeApQS1VmdFJuQ1J6YldVaTZBZzJpRU93bXF6WUhGbVNpZkFlVjk0RlAxOGtSbjl1ckV3OEpiRXJIUncrVW51L25tCmFMRHF1eGpFTVBweGhuRklCSnYwK1R3djNEVGx6TjNwUlV6dnpidGZvZCtEVTZBSmR6N3Rid1dTNmR6MHc1Z2kKbW9RelZnbFpnVDBJek9FZkV3NVpWMnRMZllHZWRlRVJ1VjhtR041c09va3R2aGxsMU1udHRaMkZNVHgySmVjUQo3K0xBRm9YVnBTS2NjbUFVZ1JBM0xOOHdVZXBVTHZZdFhiUm1QTFc4SjFINmhFeHJHTHBiTERZNmpzbGxBNlZpCk0xMjVjU0hsQWdNQkFBRUNnZ0VBQnpaRE50bmVTdWxGdk9HZlFYaHRFWGFKdWZoSzJBenRVVVpEcUNlRUxvekQKWlV6dHdxbkNRNlJLczUyandWNTN4cU9kUU94bTNMbjNvSHdNa2NZcEliWW82MjJ2dUczYnkwaVEzaFlsVHVMVgpqQmZCcS9UUXFlL2NMdngvSkczQWhFNmJxdFRjZFlXeGFmTmY2eUtpR1dzZk11WVVXTWs4MGVJVUxuRmZaZ1pOCklYNTlSOHlqdE9CVm9Sa3hjYTVoMW1ZTDFsSlJNM3ZqVHNHTHFybmpOTjNBdWZ3ZGRpK1VDbGZVL2l0K1EvZkUKV216aFFoTlRpNVFkRWJLVStOTnYvNnYvb2JvandNb25HVVBCdEFTUE05cmxFemIralQ1WHdWQjgvLzRGY3VoSwoyVzNpcjhtNHVlQ1JHSVlrbGxlLzhuQmZ0eVhiVkNocVRyZFBlaGlPM1FLQmdRRGlrR3JTOTc3cjg3Y1JPOCtQClpoeXltNXo4NVIzTHVVbFNTazJiOTI1QlhvakpZL2RRZDVTdFVsSWE4OUZKZnNWc1JRcEhHaTFCYzBMaTY1YjIKazR0cE5xcVFoUmZ1UVh0UG9GYXRuQzlPRnJVTXJXbDVJN0ZFejZnNkNQMVBXMEg5d2hPemFKZUdpZVpNYjlYTQoybDdSSFZOcC9jTDlYbmhNMnN0Q1lua2Iwd0tCZ1FEUzF4K0crakEyUVNtRVFWNXA1RnRONGcyamsyZEFjMEhNClRIQ2tTazFDRjhkR0Z2UWtsWm5ZbUt0dXFYeXNtekJGcnZKdmt2eUhqbUNYYTducXlpajBEdDZtODViN3BGcVAKQWxtajdtbXI3Z1pUeG1ZMXBhRWFLMXY4SDNINGtRNVl3MWdrTWRybVJHcVAvaTBGaDVpaGtSZS9DOUtGTFVkSQpDcnJjTzhkUVp3S0JnSHA1MzRXVWNCMVZibzFlYStIMUxXWlFRUmxsTWlwRFM2TzBqeWZWSmtFb1BZSEJESnp2ClIrdzZLREJ4eFoyWmJsZ05LblV0YlhHSVFZd3lGelhNcFB5SGxNVHpiZkJhYmJLcDFyR2JVT2RCMXpXM09PRkgKcmppb21TUm1YNmxhaDk0SjRHU0lFZ0drNGw1SHhxZ3JGRDZ2UDd4NGRjUktJWFpLZ0w2dVJSSUpBb0dCQU1CVApaL2p5WStRNTBLdEtEZHUrYU9ORW4zaGxUN3hrNXRKN3NBek5rbWdGMU10RXlQUk9Xd1pQVGFJbWpRbk9qbHdpCldCZ2JGcXg0M2ZlQ1Z4ZXJ6V3ZEM0txaWJVbWpCTkNMTGtYeGh3ZEVteFQwVit2NzZGYzgwaTNNYVdSNnZZR08KditwVVovL0F6UXdJcWZ6dlVmV2ZxdStrMHlhVXhQOGNlcFBIRyt0bEFvR0FmQUtVVWhqeFU0Ym5vVzVwVUhKegpwWWZXZXZ5TW54NWZyT2VsSmRmNzlvNGMvMHhVSjh1eFBFWDFkRmNrZW96dHNpaVFTNkN6MENRY09XVWxtSkRwCnVrdERvVzM3VmNSQU1BVjY3NlgxQVZlM0UwNm5aL2g2Tkd4Z28rT042Q3pwL0lkMkJPUm9IMFAxa2RjY1NLT3kKMUtFZlNnb1B0c1N1eEpBZXdUZmxDMXc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
+EOF
+```
+
+To create the **access-to-cafe-secret** referencegrant, copy and paste the following into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: ReferenceGrant
+metadata:
+  name: access-to-cafe-secret
+  namespace: certificate
+spec:
+  to:
+  - group: ""
+    kind: Secret
+    name: cafe-secret # if you omit this name, then Gateways in default namespace can access all Secrets in the certificate namespace
+  from:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    namespace: default
+EOF
+```
+
+To create the **cafe** gateway, copy and paste the following into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: cafe
+spec:
+  gatewayClassName: nginx
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
+  - name: https
+    port: 443
+    protocol: HTTPS
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - kind: Secret
+        name: cafe-secret
+        namespace: certificate
+EOF
+```
+
+This gateway configures:
+
+- `http` listener for HTTP traffic
+- `https` listener for HTTPS traffic. It terminates TLS connections using the `cafe-secret` we created.
+
+To create the httproute resources, copy and paste the following into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: cafe-tls-redirect
+spec:
+  parentRefs:
+  - name: cafe
+    sectionName: http
+  hostnames:
+  - "cafe.example.com"
+  rules:
+  - filters:
+    - type: RequestRedirect
+      requestRedirect:
+        scheme: https
+        port: 443
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: coffee
+spec:
+  parentRefs:
+  - name: cafe
+    sectionName: https
+  hostnames:
+  - "cafe.example.com"
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /coffee
+    backendRefs:
+    - name: coffee
+      port: 80
+EOF
+```
+
+The first route issues a `requestRedirect` from the `http` listener on port 80 to `https` on port 443. The second route binds the `coffee` route to the `https` listener.
+
+## Send Traffic
+
+Using the external IP address and port for NGINX Gateway Fabric, we can send traffic to our coffee application.
+
+{{< note >}}If you have a DNS record allocated for `cafe.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}}
+
+To test that NGINX sends an HTTPS redirect, we will send requests to the `coffee` service on the HTTP port. We
+will use curl's `--include` option to print the response headers (we are interested in the `Location` header).
+
+```shell
+curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/coffee --include
+```
+
+```text
+HTTP/1.1 302 Moved Temporarily
+...
+Location: https://cafe.example.com/coffee
+...
+```
+
+Now we will access the application over HTTPS. Since our certificate is self-signed, we will use curl's `--insecure`
+option to turn off certificate verification.
+
+```shell
+curl --resolve cafe.example.com:$GW_HTTPS_PORT:$GW_IP https://cafe.example.com:$GW_HTTPS_PORT/coffee --insecure
+```
+
+```text
+Server address: 10.244.0.6:80
+Server name: coffee-6b8b6d6486-7fc78
+```
+
+## Further Reading
+
+To learn more about redirects using the Gateway API, see the following resource:
+
+- [Gateway API Redirects](https://gateway-api.sigs.k8s.io/guides/http-redirect-rewrite/)
diff --git a/site/content/how-to/traffic-management/redirects-and-rewrites.md b/site/content/how-to/traffic-management/redirects-and-rewrites.md
new file mode 100644
index 0000000000..712986575b
--- /dev/null
+++ b/site/content/how-to/traffic-management/redirects-and-rewrites.md
@@ -0,0 +1,203 @@
+---
+title: "HTTP Redirects and Rewrites"
+description: "Learn how to redirect or rewrite your HTTP traffic using NGINX Gateway Fabric."
+weight: 400
+toc: true
+docs: "DOCS-000"
+---
+
+[HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) filters can be used to configure HTTP redirects or rewrites. Redirects return HTTP 3XX responses to a client, instructing it to retrieve a different resource. Rewrites modify components of a client request (such as hostname and/or path) before proxying it upstream.
+
+{{< note >}}NGINX Gateway Fabric currently does not support path-based redirects.{{< /note >}}
+
+To see an example of a redirect using scheme and port, see the [HTTPS Termination]({{< relref "/how-to/traffic-management/https-termination.md" >}}) guide.
+
+In this guide, we will be configuring a path URL rewrite.
+
+## Prerequisites
+
+- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric.
+- [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP address and port of NGINX Gateway Fabric into shell variables:
+
+   ```text
+   GW_IP=XXX.YYY.ZZZ.III
+   GW_PORT=<port number>
+   ```
+
+{{< note >}}In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.{{< /note >}}
+
+## Set up
+
+Create the **coffee** application in Kubernetes by copying and pasting the following block into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coffee
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: coffee
+  template:
+    metadata:
+      labels:
+        app: coffee
+    spec:
+      containers:
+      - name: coffee
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coffee
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: coffee
+EOF
+```
+
+This will create the **coffee** service and a deployment. Run the following command to verify the resources were created:
+
+```shell
+kubectl get pods,svc
+```
+
+Your output should include the **coffee** pod and the **coffee** service:
+
+```text
+NAME                          READY   STATUS      RESTARTS   AGE
+pod/coffee-6b8b6d6486-7fc78   1/1     Running   0          40s
+
+
+NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
+service/coffee       ClusterIP   10.96.189.37   <none>        80/TCP    40s
+```
+
+## Configure a Path Rewrite
+
+To create the **cafe** gateway, copy and paste the following into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: cafe
+spec:
+  gatewayClassName: nginx
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
+EOF
+```
+
+The following HTTPRoute defines two filters that will rewrite requests such as the following:
+
+- `http://cafe.example.com/coffee` to `http://cafe.example.com/beans`
+- `http://cafe.example.com/coffee/flavors` to `http://cafe.example.com/beans`
+- `http://cafe.example.com/latte/prices` to `http://cafe.example.com/prices`
+
+To create the httproute resource, copy and paste the following into your terminal:
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: coffee
+spec:
+  parentRefs:
+  - name: cafe
+    sectionName: http
+  hostnames:
+  - "cafe.example.com"
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /coffee
+    filters:
+    - type: URLRewrite
+      urlRewrite:
+        path:
+          type: ReplaceFullPath
+          replaceFullPath: /beans
+    backendRefs:
+    - name: coffee
+      port: 80
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /latte
+    filters:
+    - type: URLRewrite
+      urlRewrite:
+        path:
+          type: ReplacePrefixMatch
+          replacePrefixMatch: /
+    backendRefs:
+    - name: coffee
+      port: 80
+EOF
+```
+
+## Send Traffic
+
+Using the external IP address and port for NGINX Gateway Fabric, we can send traffic to our coffee application.
+
+{{< note >}}If you have a DNS record allocated for `cafe.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}}
+
+```shell
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee/flavors
+```
+
+Notice in the output that the URI has been rewritten:
+
+```text
+Server address: 10.244.0.6:8080
+Server name: coffee-6b8b6d6486-7fc78
+...
+URI: /beans
+```
+
+Other examples:
+
+```shell
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee
+```
+
+```text
+Server address: 10.244.0.6:8080
+Server name: coffee-6b8b6d6486-7fc78
+...
+URI: /beans
+```
+
+```shell
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/latte/prices
+```
+
+```text
+Server address: 10.244.0.6:8080
+Server name: coffee-6b8b6d6486-7fc78
+...
+URI: /prices
+```
+
+## Further Reading
+
+To learn more about redirects and rewrites using the Gateway API, see the following resource:
+
+- [Gateway API Redirects and Rewrites](https://gateway-api.sigs.k8s.io/guides/http-redirect-rewrite/)
diff --git a/site/content/how-to/traffic-management/routing-traffic-to-your-app.md b/site/content/how-to/traffic-management/routing-traffic-to-your-app.md
index 0ac01e1462..fe9c778305 100644
--- a/site/content/how-to/traffic-management/routing-traffic-to-your-app.md
+++ b/site/content/how-to/traffic-management/routing-traffic-to-your-app.md
@@ -8,8 +8,6 @@ docs: "DOCS-000"
 
 {{<custom-styles>}}
 
-## Overview
-
 You can route traffic to your Kubernetes applications using the Gateway API and NGINX Gateway Fabric. Whether you're managing a web application or a REST backend API, you can use NGINX Gateway Fabric to expose your application outside the cluster.
 
 ## Prerequisites
@@ -119,7 +117,7 @@ To create the **cafe** gateway, copy and paste the following into your terminal:
 
 ```yaml
 kubectl apply -f - <<EOF
-apiVersion: gateway.networking.k8s.io/v1beta1
+apiVersion: gateway.networking.k8s.io/v1
 kind: Gateway
 metadata:
   name: cafe
@@ -143,7 +141,7 @@ Next you will create the HTTPRoute by copying and pasting the following into you
 
 ```yaml
 kubectl apply -f - <<EOF
-apiVersion: gateway.networking.k8s.io/v1beta1
+apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: coffee
diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md
index 013f0170ce..b10133fe01 100644
--- a/site/content/overview/gateway-api-compatibility.md
+++ b/site/content/overview/gateway-api-compatibility.md
@@ -154,8 +154,9 @@ See the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) comman
     - `filters`
       - `type`: Supported.
       - `requestRedirect`: Supported except for the experimental `path` field. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest.
-      - `requestHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest.
-      - `responseHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef`: Not supported.
+      - `requestHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `urlRewrite`.
+      - `urlRewrite`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `requestHeaderModifier`.
+      - `responseHeaderModifier`, `requestMirror`, `extensionRef`: Not supported.
     - `backendRefs`: Partially supported. Backend ref `filters` are not supported.
 - `status`
   - `parents`