puppetlabs/puppet · error · ArgumentError

Format must be a String

Error message

Format must be a String

What it means

FormatParser#internal_parse is the state machine that compiles a Timespan format string; its first act is to require the argument to be a String, otherwise ArgumentError 'Format must be a String'. Puppet's Format/TimeData APIs accept either a precompiled Format object or a String - a Symbol, Array, or nil format string is rejected here.

Source

Thrown at lib/puppet/pops/time/timespan.rb:635

        _("Bad format specifier '%{expression}' in '%{format}', at position %{position}") % { expression: format[start, position - start], format: format, position: position }
      end

      def append_literal(bld, codepoint)
        if bld.empty? || !bld.last.is_a?(Format::LiteralSegment)
          bld << Format::LiteralSegment.new(''.dup.concat(codepoint))
        else
          bld.last.concat(codepoint)
        end
      end

      # States used by the #internal_parser function
      STATE_LITERAL = 0 # expects literal or '%'
      STATE_PAD = 1 # expects pad, width, or format character
      STATE_WIDTH = 2 # expects width, or format character

      def internal_parse(str)
        bld = []
        raise ArgumentError, _('Format must be a String') unless str.is_a?(String)

        highest = -1
        state = STATE_LITERAL
        padchar = '0'
        width = nil
        position = -1
        fstart = 0

        str.each_codepoint do |codepoint|
          position += 1
          if state == STATE_LITERAL
            if codepoint == 0x25 # '%'
              state = STATE_PAD
              fstart = position
              padchar = '0'
              width = nil
            else
              append_literal(bld, codepoint)

View on GitHub (pinned to e227c27540)

Solutions

  1. For Timespan, pass a single format String or a compiled Format object - arrays are not supported on this class
  2. If you got nil, default it explicitly: (fmt || '%H:%M:%S')
  3. Convert symbols/other scalars with to_s only when you know they hold a format string
  4. Check the API you targeted: Timestamp.parse supports :default and arrays, Timespan.parse supports a single Format::DEFAULTS fallback

Example fix

// before
span = Timespan.parse('12:30', formats)   # formats = ['%H:%M'] Array

// after
span = Timespan.parse('12:30', formats.first)
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, "format must be a String or Format, got #{fmt.class}" unless fmt.is_a?(String) || fmt.is_a?(Puppet::Pops::Time::Timespan::Format)
span.format(fmt)

Type guard

def valid_timespan_format?(f)
  f.is_a?(String) || f.is_a?(Puppet::Pops::Time::Timespan::Format)
end

Try / catch

begin
  Timespan.parse(str, fmt)
rescue ArgumentError => e
  raise DataError, "format arg was #{fmt.class}, expected String"
end

Prevention

When it happens

Trigger: Timespan#format(:default) (Symbol), Timespan.new(5).format(nil), or FormatParser.singleton.parse_format(42). Passing an Array of formats (legal for Timestamp) to the Timespan format path also fails here because Array is not a String. Puppet DSL: Timespan('5', ['%H','%M']) with array second arg.

Common situations: Porting code from Timestamp.parse(str, array_of_formats) to Timespan and reusing the array; passing a symbol constant or an options hash where the format string belongs; format value coming from a function that returns nil on lookup miss.

Related errors


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