Skip to content

Approval Rules Webhook Payload Shows ActiveRecord Proxy Instead of Array

Summary

When updating a merge request without changing approval rules, the webhook payload incorrectly serializes the approval_rules.previous value as an ActiveRecord association proxy object string instead of properly converting it to an array.

Example of Incorrect Behavior

{
  "approval_rules": {
    "previous": "#<ApprovalMergeRequestRule::ActiveRecord_Associations_CollectionProxy:0x0000000379856f48>",
    "current": [
      {
        "id": 1,
        "approvals_required": 0,
        "name": "All Members",
        "rule_type": "any_approver",
        // ... other fields
      }
    ]
  }
}

Expected Behavior

The previous value should be an array of approval rule objects (or an empty array if there were no previous approval rules):

{
  "approval_rules": {
    "previous": [
      {
        "id": 1,
        "approvals_required": 0,
        "name": "All Members",
        "rule_type": "any_approver",
        // ... other fields
      }
    ],
    "current": [
      {
        "id": 1,
        "approvals_required": 0,
        "name": "All Members",
        "rule_type": "any_approver",
        // ... other fields
      }
    ]
  }
}

Steps to Reproduce

  1. Create a merge request with approval rules
  2. Update the merge request via the side panel (e.g., change title, description, or any field)
  3. Do NOT change the approval rules
  4. Observe the webhook payload

Root Cause

The bug is in ee/app/models/concerns/ee/issuable.rb in the old_approval_rules method:

def old_approval_rules(assoc)
  @_old_approval_rules ||= assoc.fetch(:approval_rules, approval_rules)
end

The fallback approval_rules returns an ActiveRecord association proxy, not a converted array.

When hook_association_changes compares:

if old_approval_rules(old_associations) != approval_rules_hook_attributes
  changes[:approval_rules] = [old_approval_rules(old_associations), approval_rules_hook_attributes]
end

If :approval_rules is missing from old_associations, the fallback returns the raw proxy, which then gets included in the webhook payload.

Proposed Fix

Update the old_approval_rules method in ee/app/models/concerns/ee/issuable.rb to ensure the fallback returns converted data:

def old_approval_rules(assoc)
  @_old_approval_rules ||= assoc.fetch(:approval_rules, approval_rules.map(&:hook_attrs))
end

This ensures that even when the fallback is used, it returns properly formatted approval rule data instead of an ActiveRecord proxy.

Similar Pattern Fixed for Reviewers

This bug follows the exact same pattern that was recently fixed for reviewers in #XXXXX (reference the MR for reviewer webhooks). The reviewer fix:

  1. Simplified the fallback logic in build_standard_reviewer_changes to use fetch(:reviewers_hook_attrs, [])
  2. Ensured IssuableBaseService#associations_before_update always captures reviewers_hook_attrs

For approval rules, the same approach should be taken - the EE service override already captures the data correctly:

# ee/app/services/ee/issuable_base_service.rb
def associations_before_update(issuable)
  associations = super
  associations[:approval_rules] = issuable.approval_rules.map(&:hook_attrs) if issuable.supports_approval_rules?
  # ...
end

So the fallback should just return an empty array or already-converted data.

Impact

  • Webhook consumers receiving approval rule changes get malformed data
  • The previous value cannot be parsed as JSON
  • This breaks any automation or integrations that process approval rule changes
  • Only affects EE instances with approval rules enabled