{"record":{"id":"28724be28fbaf7cf","repo":"puppetlabs/puppet","slug":"the-persistence-hash-is-missing-the-keys-miss","errorCode":null,"errorMessage":"The 'persistence' hash is missing the keys: #{missing.join(', ')}","messagePattern":"The 'persistence' hash is missing the keys: #(.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/puppet/http/service/compiler.rb","lineNumber":166,"sourceCode":"  # @param [String] transaction_uuid The id for tracking the catalog compilation and\n  #    report submission.\n  # @param [String] job_id The id of the orchestrator job that triggered this run.\n  # @param [Hash] options A hash of options beyond direct input to catalogs. Options:\n  #    - prefer_requested_environment Whether to always override a node's classified\n  #      environment with the one supplied in the request. If this is true and no environment\n  #      is supplied, fall back to the classified environment, or finally, 'production'.\n  #    - capture_logs Whether to return the errors and warnings that occurred during\n  #      compilation alongside the catalog in the response body.\n  #    - log_level The logging level to use during the compile when capture_logs is true.\n  #      Options are 'err', 'warning', 'info', and 'debug'.\n  #\n  # @return [Array<Puppet::HTTP::Response, Puppet::Resource::Catalog, Array<String>>] An array\n  #   containing the request response, the deserialized catalog returned by\n  #   the server and array containing logs (log array will be empty if capture_logs is false)\n  #\n  def post_catalog4(certname, persistence:, environment:, facts: nil, trusted_facts: nil, transaction_uuid: nil, job_id: nil, options: nil)\n    unless persistence.is_a?(Hash) && (missing = [:facts, :catalog] - persistence.keys.map(&:to_sym)).empty?\n      raise ArgumentError, \"The 'persistence' hash is missing the keys: #{missing.join(', ')}\"\n    end\n    raise ArgumentError, \"Facts must be a Hash not a #{facts.class}\" unless facts.nil? || facts.is_a?(Hash)\n\n    body = {\n      certname: certname,\n      persistence: persistence,\n      environment: environment,\n      transaction_uuid: transaction_uuid,\n      job_id: job_id,\n      options: options\n    }\n    body[:facts] = { values: facts } unless facts.nil?\n    body[:trusted_facts] = { values: trusted_facts } unless trusted_facts.nil?\n    headers = add_puppet_headers(\n      'Accept' => get_mime_types(Puppet::Resource::Catalog).join(', '),\n      'Content-Type' => 'application/json'\n    )\n","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/http/service/compiler.rb#L148-L184","documentation":"Puppet::HTTP::Service::Compiler#post_catalog4 (the v4 catalog API) requires a persistence: keyword that must be a Hash containing at least :facts and :catalog keys, which tell the server which parts of the request to persist. ArgumentError is raised when persistence is not a Hash at all, or when either required key is absent (the message lists the missing keys).","triggerScenarios":"Calling api.post_catalog4(certname, persistence: {facts: true}, environment: env) with :catalog missing; passing persistence: nil or a String; passing keys that do not symbolize to :facts/:catalog.","commonSituations":"Code written from memory or against an older draft of the v4 API signature; copy-pasting the options hash from post_catalog (v3) calls, which has no persistence argument; tests that build the hash dynamically and skip a key under a conditional.","solutions":["Pass persistence: {facts: true, catalog: true} (or the appropriate booleans) so both required keys are present","Symbolize keys before the call: persistence.transform_keys(&:to_sym) when the hash comes from JSON or config","Fail fast in your own wrapper: reject the call unless persistence.is_a?(Hash) and both keys are present"],"exampleFix":"# before (ruby)\napi.post_catalog4(certname,\n  persistence: { 'facts' => true },\n  environment: 'production')\n# => ArgumentError: missing keys: catalog\n\n# after\napi.post_catalog4(certname,\n  persistence: { facts: true, catalog: true },\n  environment: 'production')","handlingStrategy":"validation","validationCode":"# ruby\nmissing = %i[facts catalog] - persistence.keys.map(&:to_sym)\nraise ArgumentError, \"persistence missing: #{missing.join(', ')}\" unless persistence.is_a?(Hash) && missing.empty?\napi.post_catalog4(certname, persistence: persistence, environment: env)","typeGuard":"def valid_persistence?(h)\n  h.is_a?(Hash) && (%i[facts catalog] - h.keys.map(&:to_sym)).empty?\nend","tryCatchPattern":"begin\n  api.post_catalog4(certname, persistence: persistence, environment: env)\nrescue ArgumentError => e\n  raise unless e.message.include?('persistence')\n  persistence = { facts: true, catalog: true }\n  retry\nend","preventionTips":["Construct the persistence hash inline with both keys visible: {facts: true, catalog: true}","Normalize incoming config with transform_keys(&:to_sym) before the call","Unit-test post_catalog4 argument handling so signature drift is caught in CI"],"tags":["puppet-http","catalog-api","argument-validation","required-keys"],"backgroundTag":"missing-required-parameter","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}