Skip to content

fix: compare pointer-bearing IR fields by value in Equals (HttpListenerPolicyIr, directResponse)#14331

Merged
puertomontt merged 4 commits into
kgateway-dev:mainfrom
puertomontt:omar/listenerpolicy-equals-harness
Jul 1, 2026
Merged

fix: compare pointer-bearing IR fields by value in Equals (HttpListenerPolicyIr, directResponse)#14331
puertomontt merged 4 commits into
kgateway-dev:mainfrom
puertomontt:omar/listenerpolicy-equals-harness

Conversation

@puertomontt

@puertomontt puertomontt commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes two Equals implementations that compared pointer-bearing fields by pointer identity instead of by value. Because these IRs are rebuilt from freshly-decoded objects on every recompute, equal content gets distinct pointers, so Equals spuriously reports inequality and triggers needless re-translation.

1. HttpListenerPolicyIr.Equals (listenerpolicy/http.go)

serverHeaderTransformation was compared with != (pointer identity) rather than by value. Switched to cmputils.PointerValsEqual, matching the other pointer fields in the method.

Also added an Equals harness test covering every field of HttpListenerPolicyIr, modeled on pkg/pluginsdk/ir/equality_harness_test.go. The harness's reflexivity subtest (base().Equals(base()) over two independent sets of pointers) directly guards the fixed bug.

Because all fields of HttpListenerPolicyIr are unexported, the harness's completeness check (which fails when a field is added without updating Equals) would otherwise enforce nothing. So this extends the shared equalstest harness with a non-breaking variadic IncludeUnexported() option; existing callers are unaffected.

2. directResponse.Equals (directresponse/direct_response_plugin.go)

Found while auditing every Equals method across the plugin packages for the same bug class. directResponse.Equals did d.spec == d2.spec, but DirectResponseSpec has pointer fields (Body *string, BodyFormat *shared.BodyFormat), so struct == compared those by pointer identity. Switched to value comparison with reflect.DeepEqualDirectResponseSpec is a plain (non-proto) API type, consistent with how the core IR package compares such structs (pkg/pluginsdk/ir/gatewayextension.go, backend.go). Added a harness test whose reflexivity check guards the regression.

Changes

  • pkg/kgateway/extensions2/plugins/listenerpolicy/http.go: use cmputils.PointerValsEqual for serverHeaderTransformation.
  • pkg/kgateway/extensions2/plugins/listenerpolicy/equality_harness_test.go: new harness test, one mutation case per field (28 fields) plus reflexivity/symmetry/completeness.
  • pkg/kgateway/extensions2/plugins/directresponse/direct_response_plugin.go: compare spec by value.
  • pkg/kgateway/extensions2/plugins/directresponse/equality_harness_test.go: new harness test.
  • test/testutils/equalstest/equalstest.go: add IncludeUnexported() option (variadic, backwards compatible).
  • test/testutils/equalstest/equalstest_internal_test.go: tests for the new option.

Change Type

/kind fix

Changelog

Fixed two HTTPListenerPolicy/DirectResponse cases where an IR field could be compared by pointer identity instead of value, causing spurious re-translations.

Additional Notes

Verified each harness catches its regression: temporarily reverting either fix makes the corresponding reflexivity subtest fail ("Equals(base(), base()) returned false"), then pass once restored.

Audit also surfaced (reviewed, intentionally not changed): a deliberate +krtEqualsTodo on TrafficPolicyGatewayExtensionIR.Name, an existing reflect.DeepEqual on FilterStage (behaviorally correct), and a latent (currently unreachable) nil-deref in oauthIR.Equals. The other ~38 Equals methods are clean.

…yIr.Equals

HttpListenerPolicyIr.Equals compared the serverHeaderTransformation
pointers by identity (!=) instead of by value, so two IRs with equal
transformation values but distinct pointers compared unequal, triggering
spurious re-translations. Use cmputils.PointerValsEqual instead.

Add an equals harness test covering every field of HttpListenerPolicyIr,
modeled on pkg/pluginsdk/ir/equality_harness_test.go. Since all fields are
unexported, extend the equalstest harness with a non-breaking
IncludeUnexported() option so the completeness check still fails when a
field is added without a matching Equals comparison.

Signed-off-by: omar <[email protected]>
Copilot AI review requested due to automatic review settings June 30, 2026 14:57
@gateway-bot gateway-bot added do-not-merge/description-invalid kind/fix Categorizes issue or PR as related to a bug. release-note labels Jun 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an Equals correctness bug in the listenerpolicy HTTP PolicyIR so semantically identical serverHeaderTransformation values no longer compare unequal due to pointer identity, preventing unnecessary listener re-translations. It also strengthens equality regression coverage by adding a field-by-field equality harness for HttpListenerPolicyIr, and extends the shared equalstest harness to support completeness checks for unexported fields.

Changes:

  • Fix HttpListenerPolicyIr.Equals to compare serverHeaderTransformation by value using cmputils.PointerValsEqual.
  • Add a comprehensive equality harness test for HttpListenerPolicyIr that mutates each field and includes a reflexivity guard for the pointer-identity regression.
  • Extend test/testutils/equalstest.Run with an IncludeUnexported() option and add internal tests for that behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
pkg/kgateway/extensions2/plugins/listenerpolicy/http.go Compares serverHeaderTransformation pointer values by value rather than identity in Equals.
pkg/kgateway/extensions2/plugins/listenerpolicy/equality_harness_test.go Adds a harness to ensure HttpListenerPolicyIr.Equals detects changes for every field (including reflexivity).
test/testutils/equalstest/equalstest.go Adds Option support and IncludeUnexported() to make completeness checking work for types with unexported fields.
test/testutils/equalstest/equalstest_internal_test.go Adds coverage validating default exported-only behavior and the new include-unexported behavior.
Comments suppressed due to low confidence (1)

test/testutils/equalstest/equalstest.go:109

  • The completeness-check failure message always says "exported field(s)", but Run can now be configured to include unexported fields via IncludeUnexported(). When that option is used, this message becomes misleading and can slow down debugging when a new unexported field is added without a Case.
	if len(missing) > 0 {
		t.Errorf(
			"completeness check failed for %s: exported field(s) %v are neither covered by a mutation Case nor listed as exempt — add a Case or add the field name to exempt",
			typeName(typ),
			missing,
		)
	}

Comment on lines 52 to +55
// exportedFields emits both "Embedded" (flattened) and "EmbeddedBase" (the
// embedding name itself). With covered containing only "Embedded",
// "EmbeddedBase" must appear as uncovered.
missing := uncoveredFields(typ, covered, exempt)
missing := uncoveredFields(typ, covered, exempt, false)
Comment on lines 63 to 65
// Cover every name that exportedFields produces for internalFixture.
covered := map[string]bool{"Plain": true, "Embedded": true, "EmbeddedBase": true}
exempt := map[string]bool{}
Signed-off-by: omar <[email protected]>
@puertomontt
puertomontt enabled auto-merge June 30, 2026 15:14
directResponse.Equals compared d.spec == d2.spec, but DirectResponseSpec
has pointer fields (Body, BodyFormat) so struct == compares those by
pointer identity. Since the IR is rebuilt from a freshly decoded object on
every recompute, equal specs get distinct pointers and Equals spuriously
reported inequality, triggering needless re-translation. Compare by value
with reflect.DeepEqual, matching how the core IR package compares plain
(non-proto) API structs.

Add an equals harness test; its reflexivity check guards the regression.

Signed-off-by: omar <[email protected]>
@puertomontt puertomontt changed the title fix: compare serverHeaderTransformation by value in HttpListenerPolicyIr.Equals fix: compare pointer-bearing IR fields by value in Equals (HttpListenerPolicyIr, directResponse) Jun 30, 2026
Signed-off-by: omar <[email protected]>
@puertomontt
puertomontt added this pull request to the merge queue Jul 1, 2026
Merged via the queue into kgateway-dev:main with commit 14ebe9d Jul 1, 2026
32 of 35 checks passed
@puertomontt
puertomontt deleted the omar/listenerpolicy-equals-harness branch July 1, 2026 14:26
puertomontt added a commit to puertomontt/kgateway that referenced this pull request Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/fix Categorizes issue or PR as related to a bug. release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants