Graphql recursion loop on imported incident issue type with quickactions

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

Summary

GraphQL mutations that update issues (such as issueSetLabels) trigger infinite recursion and SystemStackError when the issue description contains quick action text like /severity LOW. The quick action is re-parsed and re-executed on every update cycle, causing a stack overflow.

Steps to reproduce

  1. Create a new incident (or issue with incident type) by importing from bugzilla
  2. Edit the incident description to include the literal text /severity LOW anywhere in the description body (not executed as a quick action, just as plain text - for example, as part of imported content or documentation)
  3. Save the incident
  4. Attempt to update labels on the incident via the UI (add or remove any label)
  5. Observe HTTP 500 error in the UI with message "An error occurred while updating labels"
  6. Check production.log for SystemStackError: stack level too deep

Example Project

Support Ticket: https://gitlab-federal-support.zendesk.com/agent/tickets/14105 (US citizenship required)

What is the current bug behavior?

When an issue/incident description contains quick action syntax (e.g., /severity LOW), any subsequent update to the issue via GraphQL mutations triggers the Issues::UpdateService, which parses the description and executes the quick action. This quick action execution calls Issues::UpdateService again, which re-parses the description, finds the same quick action, executes it again, and so on - creating an infinite recursion loop until the Ruby stack is exhausted.

The result is:

  • HTTP 500 response to the client
  • SystemStackError: stack level too deep in logs
  • ~190MB memory allocation per failed request
  • ~1.9 second response time before failure
  • The issue becomes effectively uneditable via the UI for any attribute (labels, assignees, etc.)

What is the expected correct behavior?

Quick actions in issue descriptions should not be re-executed when performing unrelated updates to the issue. The expected behaviors would be:

  1. Quick actions should only be executed once when initially entered (either via description save or comment), not on every subsequent update
  2. OR the update service should detect that it's being called recursively and skip quick action parsing
  3. OR quick action parsing should be skipped when the description hasn't changed

Updating labels on an issue should succeed regardless of what text is present in the description.

Relevant logs and/or screenshots

Exception from production.log:

{
  "method": "POST",
  "path": "/gitlab/api/graphql",
  "status": 500,
  "correlation_id": "01KBK27YYSXBQY0NPN6JN6E83T",
  "exception.class": "SystemStackError",
  "exception.message": "stack level too deep",
  "exception.backtrace": [
    "activerecord (7.0.8.7) lib/active_record/associations/preloader/branch.rb:121:in `flat_map'",
    "activerecord (7.0.8.7) lib/active_record/associations/preloader/branch.rb:121:in `build_children'",
    "activerecord (7.0.8.7) lib/active_record/associations/preloader/branch.rb:17:in `initialize'",
    "activerecord (7.0.8.7) lib/active_record/associations/preloader/branch.rb:123:in `new'",
    "activerecord (7.0.8.7) lib/active_record/associations/preloader/branch.rb:123:in `block (2 levels) in build_children'",
    "..."
  ],
  "duration_s": 1.85364,
  "mem_bytes": 190103646,
  "mem_objects": 1020483
}

GraphQL operation details:

{
  "operation_name": "issueSetLabels",
  "query_fingerprint": "issueSetLabels/TndOuQaN8QWMz63U7AM1nEkVwW1X-u8EFL6Gplmn3DU=/1/...",
  "is_mutation": true,
  "variables": "{\"input\"=>{\"iid\"=>\"8635\", \"projectPath\"=>\"group/project\", \"removeLabelIds\"=>[\"gid://gitlab/GroupLabel/4376\"]}}"
}

Recursion pattern visible in full backtrace:

The backtrace shows repeated cycles through:

  • Issues::UpdateService#execute
  • QuickActions::InterpretService#execute
  • Issues::UpdateService#execute (recursive)
  • (repeating hundreds of times until stack exhausts)

Output of checks

Results of GitLab environment info

Expand for output related to GitLab environment info
GitLab version: 17.11.7
GitLab edition: Enterprise Edition
Deployment type: Omnibus
Database adapter: PostgreSQL
Database version: 14.x
Ruby version: 3.2.x (bundled with Omnibus)
Rails version: 7.0.8.7

Results of GitLab application Check

Expand for output related to the GitLab application check
All GitLab application checks pass. This is not a configuration issue - 
it is a code-level bug in the update service recursion handling.

Possible fixes

The issue likely originates in app/services/issues/update_service.rb and its interaction with app/services/quick_actions/interpret_service.rb.

Potential fixes:

  1. Skip quick action parsing when description hasn't changed: In Issues::UpdateService, only call the quick action interpreter when params[:description] is present and different from the current description.

  2. Add recursion guard: Add a flag or context variable that prevents re-entry into quick action parsing during an update cycle:

    # Pseudocode
    def execute
      return if @parsing_quick_actions
      @parsing_quick_actions = true
      # ... existing logic
    ensure
      @parsing_quick_actions = false
    end
  3. Track executed quick actions: Mark quick actions as "consumed" after first execution so they aren't re-executed on subsequent updates. This could be done by escaping or removing the quick action syntax after execution.

  4. Scope quick action parsing to specific update paths: Only parse quick actions when the update originates from a description change or comment, not from label/assignee/milestone updates.

Related code paths to investigate:

  • app/services/issues/update_service.rb
  • app/services/quick_actions/interpret_service.rb
  • app/services/issuable_base_service.rb
  • ee/app/services/ee/issues/update_service.rb (if EE-specific logic is involved)

Patch release information for backports

This bug affects the usability of issues/incidents that contain quick action syntax in their descriptions. It was discovered when importing issues from Bugzilla where severity fields were converted to /severity text in descriptions.

Impact assessment:

  • Issues become uneditable once they contain quick action text in description
  • Affects all GraphQL mutations that trigger Issues::UpdateService
  • No data loss, but significant UX impact
  • Workaround exists (manually remove quick action text via Rails console using update_column)
Edited by 🤖 GitLab Bot 🤖