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
- For Timespan, pass a single format String or a compiled Format object - arrays are not supported on this class
- If you got nil, default it explicitly: (fmt || '%H:%M:%S')
- Convert symbols/other scalars with to_s only when you know they hold a format string
- 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
- Timespan formats: one String or compiled Format - never an Array (that is the Timestamp API)
- Default nil formats explicitly: (fmt || '%H:%M:%S')
- Type-check at module parameter boundaries before values reach Puppet internals
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
- %{klass} cannot be added to a Timespan
- %{klass} cannot be subtracted from a Timespan
- Can not do modulus on a Timespan using a %{klass}
- A Timespan cannot be divided by %{klass}
- Unable to parse '%{timespan}' using format '%{format}'
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/4c2675deb8d02f20.
Report an issue: GitHub.