Skip to content

fix: clone request state in response.To#315

Open
immanuwell wants to merge 1 commit into
crossplane:mainfrom
immanuwell:fix/clone-response-to-state
Open

fix: clone request state in response.To#315
immanuwell wants to merge 1 commit into
crossplane:mainfrom
immanuwell:fix/clone-response-to-state

Conversation

@immanuwell
Copy link
Copy Markdown

Description of your changes

Fixes #314

response.To said it copied request state, but it was still sharing Desired and Context with the request. So a function could update rsp after To(...) and accidentally mutate req too. Kinda sneaky bug.

This clones both fields before returning, so rsp is its own thing now. nice and boring.

I have:

  • Read and followed Crossplane's [contribution process].
  • Run make reviewable to ensure this PR is ready for review.

How has this code been tested

Repro on old main:

  1. Build a request with Desired and Context.
  2. Call rsp := response.To(req, time.Minute).
  3. Add a context key or desired resource to rsp.
  4. req changes too. not ideal.

Ran:

  • go test ./response -run TestToClonesRequestState -count=1
  • go test ./...

Signed-off-by: immanuwell <pchpr.00@list.ru>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes an aliasing bug where response.To() returned a response sharing pointers with the input request. The function now uses proto.Clone() to deep-copy the request's Desired state and Context fields, ensuring mutations to the response remain independent of the original request.

Changes

Request State Cloning

Layer / File(s) Summary
Deep-copy request state in To() function
response/response.go, response/response_test.go
To() imports proto and clones req.GetDesired() and req.GetContext() into independent instances before assigning them to the response. TestToClonesRequestState validates that mutations to response context and desired resources do not affect the original request's state.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Breaking Changes ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: clone request state in response.To' is descriptive and clearly summarizes the main change—implementing deep cloning of request state in the response.To function.
Description check ✅ Passed The description clearly relates to the changeset, explaining the bug (shared pointers in Desired and Context), the fix (deep cloning), and providing reproduction steps.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #314 by cloning both Desired and Context fields before returning the response, preventing accidental mutation of the original request.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objective: modifying response.To to clone request state and adding a test to verify the cloning behavior. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
response/response.go (2)

46-49: 💤 Low value

Consider safer type assertion for cloned Context.

Similar to the Desired state handling, the type assertion for Context could benefit from a checked assertion pattern. However, this is a minor defensive coding suggestion rather than a critical issue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@response/response.go` around lines 46 - 49, The cloned Context should use a
checked type assertion to avoid panics: replace the direct assertion in the
req.GetContext() block with a two-value assertion from proto.Clone(c) into a
local variable (e.g., v, ok := proto.Clone(c).(*structpb.Struct)); if ok set ctx
= v, otherwise leave ctx nil (or log/handle the unexpected type) so the code
mirrors the safer pattern used for Desired state handling.

41-44: 💤 Low value

Consider safer type assertion for cloned Desired state.

The type assertion proto.Clone(d).(*v1.State) could panic if proto.Clone returns an unexpected type. While proto.Clone should preserve the concrete type, using a checked type assertion would make this more defensive:

var desired *v1.State
if d := req.GetDesired(); d != nil {
    cloned, ok := proto.Clone(d).(*v1.State)
    if !ok {
        // This should never happen, but guards against unexpected proto.Clone behavior
        panic("proto.Clone returned unexpected type for Desired state")
    }
    desired = cloned
}

That said, if the current pattern is established elsewhere in the codebase and proto.Clone is guaranteed to preserve types, this may be acceptable as-is. What's the team's preference for type assertions with protobuf cloning?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@response/response.go` around lines 41 - 44, The unchecked type assertion
proto.Clone(d).(*v1.State) can panic if the clone's concrete type differs;
update the desired construction (around req.GetDesired and proto.Clone) to use a
checked assertion: store proto.Clone(d) in a variable, perform a two-value type
assertion to *v1.State, handle the !ok branch (either panic with a clear message
or return an error) and only assign desired when ok is true to make the code
defensive against unexpected proto.Clone results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@response/response_test.go`:
- Around line 34-71: The test TestToClonesRequestState should be refactored into
a table-driven test using the args/want pattern: define inner types args { req
*v1.RunFunctionRequest; ttl time.Duration } and want { contextFieldsMatch bool;
resourcesMatch bool }, create a map[string]struct{ reason string; args args;
mutate func(*v1.RunFunctionResponse); want want } with cases (e.g., mutating
context, mutating desired resources), loop with t.Run(caseName, func(t
*testing.T){ rsp := To(tc.args.req, tc.args.ttl); tc.mutate(rsp); assert that
req.GetContext().GetFields() and req.GetDesired().GetResources() are unchanged
compared to expectations }), and use SetContextKey and rsp.Desired mutation
inside mutate funcs to exercise behavior; ensure the test uses t.Run per case
and follows the existing PascalCase naming convention for the top-level test
function (TestToClonesRequestState) and case names.

---

Nitpick comments:
In `@response/response.go`:
- Around line 46-49: The cloned Context should use a checked type assertion to
avoid panics: replace the direct assertion in the req.GetContext() block with a
two-value assertion from proto.Clone(c) into a local variable (e.g., v, ok :=
proto.Clone(c).(*structpb.Struct)); if ok set ctx = v, otherwise leave ctx nil
(or log/handle the unexpected type) so the code mirrors the safer pattern used
for Desired state handling.
- Around line 41-44: The unchecked type assertion proto.Clone(d).(*v1.State) can
panic if the clone's concrete type differs; update the desired construction
(around req.GetDesired and proto.Clone) to use a checked assertion: store
proto.Clone(d) in a variable, perform a two-value type assertion to *v1.State,
handle the !ok branch (either panic with a clear message or return an error) and
only assign desired when ok is true to make the code defensive against
unexpected proto.Clone results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fb82b9b0-731c-4019-958e-52ed980562c4

📥 Commits

Reviewing files that changed from the base of the PR and between 78969ff and 02ee1d1.

📒 Files selected for processing (2)
  • response/response.go
  • response/response_test.go

Comment thread response/response_test.go
Comment on lines +34 to +71
func TestToClonesRequestState(t *testing.T) {
req := &v1.RunFunctionRequest{
Meta: &v1.RequestMeta{Tag: "original"},
Desired: &v1.State{
Resources: map[string]*v1.Resource{
"existing": {
Resource: resource.MustStructJSON(`{
"apiVersion": "example.org/v1",
"kind": "Widget"
}`),
},
},
},
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"existing": structpb.NewStringValue("value"),
},
},
}

rsp := To(req, time.Minute)

SetContextKey(rsp, "new", structpb.NewStringValue("response-only"))
rsp.Desired.Resources["response-only"] = &v1.Resource{
Resource: resource.MustStructJSON(`{
"apiVersion": "example.org/v1",
"kind": "ExtraWidget"
}`),
}

if got, ok := req.GetContext().GetFields()["new"]; ok {
t.Fatalf("To(...) aliased request context: unexpected field %q", got.GetStringValue())
}

if _, ok := req.GetDesired().GetResources()["response-only"]; ok {
t.Fatal("To(...) aliased desired resources from the request")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Consider refactoring to table-driven test structure.

While this test effectively validates the cloning behavior, the coding guidelines require table-driven test structure with args/want pattern for consistency with the rest of the codebase. Looking at the other tests in this file (e.g., TestSetDesiredResources, TestOutput), they all follow the table-driven pattern.

You could structure this as:

func TestToClonesRequestState(t *testing.T) {
    type args struct {
        req *v1.RunFunctionRequest
        ttl time.Duration
    }
    type want struct {
        contextFieldsMatch  bool
        resourcesMatch      bool
    }
    cases := map[string]struct {
        reason string
        args   args
        mutate func(*v1.RunFunctionResponse)
        want   want
    }{
        "MutatingContextDoesNotAffectRequest": {
            reason: "Adding fields to response context should not modify request context",
            args: args{
                req: &v1.RunFunctionRequest{
                    Context: &structpb.Struct{
                        Fields: map[string]*structpb.Value{
                            "existing": structpb.NewStringValue("value"),
                        },
                    },
                },
                ttl: time.Minute,
            },
            mutate: func(rsp *v1.RunFunctionResponse) {
                SetContextKey(rsp, "new", structpb.NewStringValue("response-only"))
            },
            want: want{
                contextFieldsMatch: true,
                resourcesMatch:     true,
            },
        },
        // Additional cases for mutating Desired, both fields, etc.
    }
    // ... test implementation
}

This would align with the established pattern and make it easier to add more mutation scenarios in the future. What do you think about this approach?

As per coding guidelines, **/*_test.go: Enforce table-driven test structure: PascalCase test names (no underscores), args/want pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@response/response_test.go` around lines 34 - 71, The test
TestToClonesRequestState should be refactored into a table-driven test using the
args/want pattern: define inner types args { req *v1.RunFunctionRequest; ttl
time.Duration } and want { contextFieldsMatch bool; resourcesMatch bool },
create a map[string]struct{ reason string; args args; mutate
func(*v1.RunFunctionResponse); want want } with cases (e.g., mutating context,
mutating desired resources), loop with t.Run(caseName, func(t *testing.T){ rsp
:= To(tc.args.req, tc.args.ttl); tc.mutate(rsp); assert that
req.GetContext().GetFields() and req.GetDesired().GetResources() are unchanged
compared to expectations }), and use SetContextKey and rsp.Desired mutation
inside mutate funcs to exercise behavior; ensure the test uses t.Run per case
and follows the existing PascalCase naming convention for the top-level test
function (TestToClonesRequestState) and case names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

response.To aliases request desired state and context

1 participant