Repository: nginx/nginx-gateway-fabric

Issue:
"client_max_body_size" directive is duplicate for a ClientSettingsPolicy attached to an HTTPRoute with multiple rules
**Describe the bug**
Targeting a `ClientSettingsPolicy` having `spec.body.maxSize` to an HTTPRoute with multiple rules generates invalid config that cannot be applied to nginx.

`"client_max_body_size" directive is duplicate in /etc/nginx/includes/ClientSettingsPolicy_myroute_httproute-client-settings.conf:2`

The same policy, when attached to an HTTPRoute with a single rule or when attached to a Gateway, does not cause invalid config to be generated.

**To Reproduce**
Steps to reproduce the behavior:
1. Create an `HTTPRoute` with a single rule.
   ```yaml
   apiVersion: gateway.networking.k8s.io/v1
   kind: HTTPRoute
   metadata:
     name: mydomain
     namespace: default
   spec:
     hostnames:
       - mydomain.example.cloud
     parentRefs:
       - name: defaultgateway
         sectionName: https
     rules:
       # fallback for all other requests
       - backendRefs:
           - name: mybackend
             port: 80
         filters:
           - type: ExtensionRef
             extensionRef:
               group: gateway.nginx.org
               kind: SnippetsFilter
               name: proxy-timeouts
         sessionPersistence:
           sessionName: SESSIONID
           type: Cookie
   ```
2. Create a `ClientSettingsPolicy` and specify `spec.body.maxSize: 250m`
   ```yaml
   apiVersion: gateway.nginx.org/v1alpha1
   kind: ClientSettingsPolicy
   metadata:
     name: httproute-client-settings
     namespace: default
   spec:
     targetRef:
       group: gateway.networking.k8s.io
       kind: HTTPRoute
       name: mydomain
       # kind: Gateway
       # name: defaultgateway
     body:
       maxSize: "250m"
   ```
3. Observe that agent is configured correctly.
4. Reconfigure the HTTPRoute to contain multiple rules.
   ```yaml
   apiVersion: gateway.networking.k8s.io/v1
   kind: HTTPRoute
   metadata:
     name: mydomain
     namespace: default
   spec:
     hostnames:
       - mydomain.example.cloud
     parentRefs:
       - name: defaultgateway
         sectionName: https
     rules:
       # match with more headers matches first
       # /rest path with Referer header matching regex -> goes to app
       - matches:
         - path:
             type: PathPrefix
             value: /rest
           headers:
             - name: Referer
               type: RegularExpression
               value: "(?i)(mydomain|myotherdomain).+\\.example\\.(cloud|com)"
         backendRefs:
           - name: mybackend
             port: 80
         filters:
           - type: ExtensionRef
             extensionRef:
               group: gateway.nginx.org
               kind: SnippetsFilter
               name: proxy-timeouts
         sessionPersistence:
           sessionName: SESSIONID
           type: Cookie
       # /rest path with any other Referer header -> goes to api
       - matches:
           - path:
               type: PathPrefix
               value: /rest
         backendRefs:
           - name: mybackend-api
             port: 80
         filters:
           - type: ExtensionRef
             extensionRef:
               group: gateway.nginx.org
               kind: SnippetsFilter
               name: proxy-timeouts
         sessionPersistence:
           sessionName: SESSIONID
           type: Cookie
       # fallback for all other requests
       - backendRefs:
           - name: mybackend
             port: 80
         filters:
           - type: ExtensionRef
             extensionRef:
               group: gateway.nginx.org
               kind: SnippetsFilter
               name: proxy-timeouts
         sessionPersistence:
           sessionName: SESSIONID
           type: Cookie
   ```
5. Observe that invalid configuration is generated and routing fails.

**Expected behavior**
When attaching a `ClientSettingsPolicy` to an HTTPRoute, valid configuration should always be generated, even if multiple rules are present.

**Your environment**
* Version of the NGINX Gateway Fabric: 2.1.0
* Version of Kubernetes: 1.31.3
* Kubernetes platform: AKS
* Details on how you expose the NGINX Gateway Fabric Pod: LoadBalancer Service with Azure annotations

**Additional context**
```
{"level":"debug","ts":"2025-08-19T19:07:02Z","logger":"nginxUpdater.fileService","msg":"Getting file for agent","file":"/etc/nginx/conf.d/http.conf"}
{"level":"debug","ts":"2025-08-19T19:07:02Z","logger":"nginxUpdater.fileService","msg":"Getting file for agent","file":"/etc/nginx/conf.d/matches.json"}
{"level":"error","ts":"2025-08-19T19:07:02Z","logger":"nginxUpdater.commandService","msg":"error sending request to agent","error":"msg: Config apply failed, rolling back config; error: failed validating config NGINX config test failed exit status 1: 2025/08/19 19:07:02 [emerg] 766#766: \"client_max_body_size\" directive is duplicate in /etc/nginx/includes/ClientSettingsPolicy_mydomain_httproute-client-settings.conf:2\nnginx: [emerg] \"client_max_body_size\" directive is duplicate in /etc/nginx/includes/ClientSettingsPolicy_mydomain_httproute-client-settings.conf:2\nnginx: configuration file /etc/nginx/nginx.conf test failed\n","stacktrace":"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent.(*commandService).logAndSendErrorStatus\n\t/home/runner/work/nginx-gateway-fabric/nginx-gateway-fabric/internal/controller/nginx/agent/command.go:365\ngithub.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent.(*commandService).setInitialConfig\n\t/home/runner/work/nginx-gateway-fabric/nginx-gateway-fabric/internal/controller/nginx/agent/command.go:322\ngithub.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent.(*commandService).Subscribe\n\t/home/runner/work/nginx-gateway-fabric/nginx-gateway-fabric/internal/controller/nginx/agent/command.go:149\ngithub.com/nginx/agent/v3/api/grpc/mpi/v1._CommandService_Subscribe_Handler\n\tpkg/mod/github.com/nginx/agent/v3@v3.2.1/api/grpc/mpi/v1/command_grpc.pb.go:233\ngithub.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent/grpc/interceptor.(*ContextSetter).Stream.ContextSetter.Stream.func1\n\t/home/runner/work/nginx-gateway-fabric/nginx-gateway-fabric/internal/controller/nginx/agent/grpc/interceptor/interceptor.go:65\ngoogle.golang.org/grpc.(*Server).processStreamingRPC\n\tpkg/mod/google.golang.org/grpc@v1.74.2/server.go:1728\ngoogle.golang.org/grpc.(*Server).handleStream\n\tpkg/mod/google.golang.org/grpc@v1.74.2/server.go:1845\ngoogle.golang.org/grpc.(*Server).serveStreams.func2.1\n\tpkg/mod/google.golang.org/grpc@v1.74.2/server.go:1061"}
{"level":"error","ts":"2025-08-19T19:07:02Z","logger":"eventHandler","msg":"Failed to update NGINX configuration","error":"msg: Config apply failed, rolling back config; error: failed validating config NGINX config test failed exit status 1: 2025/08/19 19:07:02 [emerg] 766#766: \"client_max_body_size\" directive is duplicate in /etc/nginx/includes/ClientSettingsPolicy_mydomain_httproute-client-settings.conf:2\nnginx: [emerg] \"client_max_body_size\" directive is duplicate in /etc/nginx/includes/ClientSettingsPolicy_mydomain_httproute-client-settings.conf:2\nnginx: configuration file /etc/nginx/nginx.conf test failed\n","stacktrace":"github.com/nginx/nginx-gateway-fabric/v2/internal/controller.(*eventHandlerImpl).waitForStatusUpdates\n\t/home/runner/work/nginx-gateway-fabric/nginx-gateway-fabric/internal/controller/handler.go:262"}
```

Hints from issue comments:
@nginx-bot: Hi @mattmichal! Welcome to the project! 🎉

Thanks for opening this issue!
Be sure to check out our [Contributing Guidelines](../blob/main/CONTRIBUTING.md) and the [Issue Lifecycle](../blob/main/ISSUE_LIFECYCLE.md) while you wait for someone on the team to take a look at this.

<!-- nginx-bot-comment -->@sjberman: Hey @mattmichal, we're investigating this now, thanks for opening the issue.

Sidenote, I see you have sessionPersistence specified in your HTTPRoute, which is something we don't yet support. I'll take note that we need to have better validation on fields we don't support and reject those.@mattmichal: Thanks for investigating!

> Sidenote, I see you have sessionPersistence specified in your HTTPRoute, which is something we don't yet support.

Is there an issue I can track for adding support?@sjberman: We have kind of a placeholder [here](https://github.com/nginx/nginx-gateway-fabric/issues/2676), but it's not fleshed out yet and we haven't discussed. I don't think it was on our roadmap in the near future, but priorities can change, especially when it's a feature that we know people want!@shaun-nx: Hi @mattmichal 
Good news. We believe we've found the culprit. There's a branch here with the changes if you want to test pull down the branch and test it out locally as well: https://github.com/nginx/nginx-gateway-fabric/compare/main...bug/clientsettingspolicy-multiple-route-rule@shaun-nx: Hi @mattmichal 
The fix for this has been merged into `main` now 🎉 
Thanks again for reporting this. This is the kind of feedback that makes sure this implementation is as good as it can be 😄 

This fix will also be included in our upcoming 2.1.1 release which should be out next week 🚀 @mattmichal: I tested this with the 2.1.1 release and my issue is resolved. Thanks, all!@shaun-nx: Nice one @mattmichal !
Thanks again for finding and reporting this 🎉 

Solution patch:
diff --git a/internal/controller/state/dataplane/configuration.go b/internal/controller/state/dataplane/configuration.go
index 9b93aa7f..52306f4e 100644
--- a/internal/controller/state/dataplane/configuration.go
+++ b/internal/controller/state/dataplane/configuration.go
@@ -591,10 +591,10 @@ func (hpr *hostPathRules) upsertRoute(
 				if !exist {
 					hostRule.Path = path
 					hostRule.PathType = convertPathType(*m.Path.Type)
+					hostRule.Policies = append(hostRule.Policies, pols...)
 				}
 
 				hostRule.GRPC = GRPC
-				hostRule.Policies = append(hostRule.Policies, pols...)
 
 				hostRule.MatchRules = append(hostRule.MatchRules, MatchRule{
 					Source:       objectSrc,



Test patch:
diff --git a/internal/controller/state/dataplane/configuration_test.go b/internal/controller/state/dataplane/configuration_test.go
index 8e600787..b329b9d4 100644
--- a/internal/controller/state/dataplane/configuration_test.go
+++ b/internal/controller/state/dataplane/configuration_test.go
@@ -680,6 +680,37 @@ func TestBuildConfiguration(t *testing.T) {
 		},
 	)
 
+	hrAdvancedRouteWithPolicyAndHeaderMatch,
+		groupsHRAdvancedWithHeaderMatch,
+		routeHRAdvancedWithHeaderMatch := createTestResources(
+		"hr-advanced-route-with-policy-header-match",
+		"policy.com",
+		"listener-80-1",
+		pathAndType{path: "/rest", pathType: prefix},
+	)
+
+	pathMatch := helpers.GetPointer(v1.HTTPPathMatch{
+		Value: helpers.GetPointer("/rest"),
+		Type:  helpers.GetPointer(v1.PathMatchPathPrefix),
+	})
+
+	routeHRAdvancedWithHeaderMatch.Spec.Rules[0].Matches = []v1.HTTPRouteMatch{
+		{
+			Path: pathMatch,
+			Headers: []v1.HTTPHeaderMatch{
+				{
+					Name:  "Referrer",
+					Type:  helpers.GetPointer(v1.HeaderMatchRegularExpression),
+					Value: "(?i)(mydomain|myotherdomain).+\\.example\\.(cloud|com)",
+				},
+			},
+		},
+		{
+			Path: pathMatch,
+		},
+	}
+	routeHRAdvancedWithHeaderMatch.Spec.Hostnames = []v1.Hostname{"policy.com"}
+
 	l7RouteWithPolicy.Policies = []*graph.Policy{hrPolicy1, invalidPolicy}
 
 	httpsHRWithPolicy, expHTTPSHRWithPolicyGroups, l7HTTPSRouteWithPolicy := createTestResources(
@@ -2346,6 +2377,74 @@ func TestBuildConfiguration(t *testing.T) {
 			}),
 			msg: "Simple Gateway and HTTPRoute with policies attached",
 		},
+		{
+			graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph {
+				gw := g.Gateways[gatewayNsName]
+				gw.Listeners = append(gw.Listeners, []*graph.Listener{
+					{
+						Name:        "listener-80-1",
+						GatewayName: gatewayNsName,
+						Source:      listener80,
+						Valid:       true,
+						Routes: map[graph.RouteKey]*graph.L7Route{
+							graph.CreateRouteKey(hrAdvancedRouteWithPolicyAndHeaderMatch): routeHRAdvancedWithHeaderMatch,
+						},
+					},
+				}...)
+				gw.Policies = []*graph.Policy{gwPolicy1, gwPolicy2}
+				routeHRAdvancedWithHeaderMatch.Policies = []*graph.Policy{hrPolicy1}
+				g.Routes = map[graph.RouteKey]*graph.L7Route{
+					graph.CreateRouteKey(hrAdvancedRouteWithPolicyAndHeaderMatch): routeHRAdvancedWithHeaderMatch,
+				}
+				return g
+			}),
+			expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration {
+				conf.SSLServers = []VirtualServer{}
+				conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{}
+				conf.HTTPServers = []VirtualServer{
+					{
+						IsDefault: true,
+						Port:      80,
+						Policies:  []policies.Policy{gwPolicy1.Source, gwPolicy2.Source},
+					},
+					{
+						Hostname: "policy.com",
+						PathRules: []PathRule{
+							{
+								Path:     "/rest",
+								PathType: PathTypePrefix,
+								MatchRules: []MatchRule{
+									{
+										BackendGroup: groupsHRAdvancedWithHeaderMatch[0],
+										Source:       &hrAdvancedRouteWithPolicyAndHeaderMatch.ObjectMeta,
+										Match: Match{
+											Headers: []HTTPHeaderMatch{
+												{
+													Name:  "Referrer",
+													Value: "(?i)(mydomain|myotherdomain).+\\.example\\.(cloud|com)",
+													Type:  "RegularExpression",
+												},
+											},
+										},
+									},
+									{
+										BackendGroup: groupsHRAdvancedWithHeaderMatch[0],
+										Source:       &hrAdvancedRouteWithPolicyAndHeaderMatch.ObjectMeta,
+									},
+								},
+								Policies: []policies.Policy{hrPolicy1.Source},
+							},
+						},
+						Port:     80,
+						Policies: []policies.Policy{gwPolicy1.Source, gwPolicy2.Source},
+					},
+				}
+				conf.Upstreams = []Upstream{fooUpstream}
+				conf.BackendGroups = []BackendGroup{groupsHRAdvancedWithHeaderMatch[0]}
+				return conf
+			}),
+			msg: "Gateway and HTTPRoute with policies attached with advanced routing",
+		},
 		{
 			graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph {
 				gw := g.Gateways[gatewayNsName]