puppetlabs/puppet · error · Puppet::DataBinding::RecursiveLookupError

Recursive lookup detected in [%{name_stack}]

Error message

Recursive lookup detected in [%{name_stack}]

What it means

Invocation#check guards nested named lookups: every provider key lookup (data_provider.rb, module_data_provider.rb) and every interpolation (interpolation.rb) pushes the key onto @name_stack for the duration of resolving it. If the same name appears twice on the stack, resolution is cyclical and Puppet::DataBinding::RecursiveLookupError is raised with the full stack joined by ', '. It converts any nested Puppet::Error (other than LookupError) into a LookupError carrying the cause, so recursion never becomes an infinite loop.

Source

Thrown at lib/puppet/pops/lookup/invocation.rb:93

    key = LookupKey.new(key) unless key.is_a?(LookupKey)
    @top_key = key
    @module_name = module_name.nil? ? key.module_name : module_name
    save_current = self.class.current
    if save_current.equal?(self)
      yield
    else
      begin
        self.class.current = self
        yield
      ensure
        self.class.current = save_current
      end
    end
  end

  def check(name)
    if @name_stack.include?(name)
      raise Puppet::DataBinding::RecursiveLookupError, _("Recursive lookup detected in [%{name_stack}]") % { name_stack: @name_stack.join(', ') }
    end
    return unless block_given?

    @name_stack.push(name)
    begin
      yield
    rescue Puppet::DataBinding::LookupError
      raise
    rescue Puppet::Error => detail
      raise Puppet::DataBinding::LookupError.new(detail.message, nil, nil, nil, detail)
    ensure
      @name_stack.pop
    end
  end

  def emit_debug_info(preamble)
    @explainer.emit_debug_info(preamble) if @explainer.is_a?(DebugExplainer)
  end

View on GitHub (pinned to e227c27540)

Solutions

  1. Read the bracketed name stack in the message — it spells out the exact cycle (e.g. [a, b, a]); break the loop at the last link.
  2. Give the self-referencing value a different key (mykey_base) and interpolate the base key from mykey.
  3. For structural reuse, use %{alias('other_key')} pointing at a *different* key, never the containing one.
  4. Re-run `puppet lookup --explain <key>` after the fix to confirm the chain terminates.

Example fix

# before: data/common.yaml
myapp::config: "%{lookup('myapp::config')}/extra"

# after: data/common.yaml
myapp::config_base: "default"
myapp::config: "%{lookup('myapp::config_base')}/extra"
Defensive patterns

Strategy: validation

Validate before calling

# Static check: no data value may interpolate its own key
require 'yaml'

Dir['**/data/**/*.yaml'].each do |f|
  YAML.load_file(f).to_a.each do |key, value|
    next unless value.is_a?(String)
    if value.match?(/%\{(?:alias\(|lookup\()?['"]#{Regexp.escape(key.to_s)}['"]/)
      abort "#{f}: key '#{key}' interpolates itself"
    end
  end
end

Try / catch

begin
  $v = lookup('myapp::config')
rescue Puppet::DataBinding::RecursiveLookupError => e
  # e.message shows the cycle, e.g. [a, b, a] — alert data owners, do not loop
  notify { "recursive data cycle: #{e.message}": }
  $v = undef
end

Prevention

When it happens

Trigger: Data value that interpolates itself: `mykey: "prefix_%{lookup('mykey')}"`; a %{alias('mykey')} inside mykey's own value; key A interpolates key B which interpolates key A (the name_stack shows the exact cycle); a lookup_options entry whose resolution triggers a lookup of the same key.

Common situations: Refactoring hiera data so a key inherits from itself; typo where the interpolated key equals the containing key; chains of alias() across hierarchy layers that loop; templating logic calling lookup() on the enclosing key.

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/204c6d2488d07d99. Report an issue: GitHub.