puppetlabs/puppet · error · ArgumentError
Facts must be a Hash not a #{facts.class}
Error message
Facts must be a Hash not a #{facts.class} What it means
post_catalog4 only accepts facts: as nil (omitted) or a Hash of fact name to value; anything else is rejected with ArgumentError before the request body is built. The facts hash is wrapped as body[:facts] = { values: facts } for the v4 wire format, so a non-Hash cannot be serialized.
Source
Thrown at lib/puppet/http/service/compiler.rb:168
# @param [String] job_id The id of the orchestrator job that triggered this run.
# @param [Hash] options A hash of options beyond direct input to catalogs. Options:
# - prefer_requested_environment Whether to always override a node's classified
# environment with the one supplied in the request. If this is true and no environment
# is supplied, fall back to the classified environment, or finally, 'production'.
# - capture_logs Whether to return the errors and warnings that occurred during
# compilation alongside the catalog in the response body.
# - log_level The logging level to use during the compile when capture_logs is true.
# Options are 'err', 'warning', 'info', and 'debug'.
#
# @return [Array<Puppet::HTTP::Response, Puppet::Resource::Catalog, Array<String>>] An array
# containing the request response, the deserialized catalog returned by
# the server and array containing logs (log array will be empty if capture_logs is false)
#
def post_catalog4(certname, persistence:, environment:, facts: nil, trusted_facts: nil, transaction_uuid: nil, job_id: nil, options: nil)
unless persistence.is_a?(Hash) && (missing = [:facts, :catalog] - persistence.keys.map(&:to_sym)).empty?
raise ArgumentError, "The 'persistence' hash is missing the keys: #{missing.join(', ')}"
end
raise ArgumentError, "Facts must be a Hash not a #{facts.class}" unless facts.nil? || facts.is_a?(Hash)
body = {
certname: certname,
persistence: persistence,
environment: environment,
transaction_uuid: transaction_uuid,
job_id: job_id,
options: options
}
body[:facts] = { values: facts } unless facts.nil?
body[:trusted_facts] = { values: trusted_facts } unless trusted_facts.nil?
headers = add_puppet_headers(
'Accept' => get_mime_types(Puppet::Resource::Catalog).join(', '),
'Content-Type' => 'application/json'
)
url = URI::HTTPS.build(host: @url.host, port: @url.port, path: Puppet::Util.uri_encode("/puppet/v4/catalog"))
response = @client.post(View on GitHub (pinned to e227c27540)
Solutions
- Pass facts.values when you hold a Puppet::Node::Facts object
- Ensure the facts came from JSON.parse or YAML.safe_load, not a raw String
- Omit facts: entirely when you do not want to send facts
Example fix
# before (ruby) api.post_catalog4(certname, persistence: p, environment: env, facts: node_facts) # Puppet::Node::Facts => ArgumentError # after api.post_catalog4(certname, persistence: p, environment: env, facts: node_facts.values) # Hash
Defensive patterns
Strategy: type-guard
Validate before calling
# ruby facts = facts.values if facts.is_a?(Puppet::Node::Facts) raise ArgumentError, 'facts must be a Hash' unless facts.nil? || facts.is_a?(Hash)
Type guard
def facts_hash?(f) f.nil? || f.is_a?(Hash) end
Try / catch
begin
api.post_catalog4(certname, persistence: p, environment: env, facts: facts)
rescue ArgumentError => e
raise unless e.message.include?('Facts must be a Hash')
facts = facts.values if facts.respond_to?(:values)
retry
end Prevention
- Unwrap Puppet::Node::Facts with .values at the boundary of your code
- Parse serialized facts once (JSON.parse) and pass the resulting Hash, never a String
- Assert facts: is a Hash in specs for every call site
When it happens
Trigger: Calling api.post_catalog4(..., facts: some_facts_object) with a Puppet::Node::Facts instance instead of its .values hash; facts: as a raw JSON String; facts: as an Array of pairs.
Common situations: Reusing a Puppet::Node::Facts object from the indirector where the API wants the raw values hash; passing a serialized JSON string instead of the parsed hash; custom test fixtures using Arrays or OpenStructs.
Related errors
- The 'persistence' hash is missing the keys: #{missing.join('
- puppet.tasks/unparseable-metadata
- Catalog environment didn't stabilize after %{tries} fetches,
- Could not retrieve local facts: %{detail}
- Expected PCallableType or PVariantType thereof, got %{type_c
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/04b79c6e752fa31e.
Report an issue: GitHub.