{"record":{"id":"0919bbc3bcc3da33","repo":"puppetlabs/puppet","slug":"could-not-destroy-json-request-detail","errorCode":null,"errorMessage":"Could not destroy %{json} %{request}: %{detail}","messagePattern":"Could not destroy %(.+?) %(.+?): %(.+?)","errorType":"exception","errorClass":"Puppet::Error","httpStatus":null,"severity":"error","filePath":"lib/puppet/indirector/json.rb","lineNumber":28,"sourceCode":"class Puppet::Indirector::JSON < Puppet::Indirector::Terminus\n  def find(request)\n    load_json_from_file(path(request.key), request.key)\n  end\n\n  def save(request)\n    filename = path(request.key)\n    FileUtils.mkdir_p(File.dirname(filename))\n\n    Puppet::FileSystem.replace_file(filename, 0o660) { |f| f.print to_json(request.instance).force_encoding(Encoding::BINARY) }\n  rescue TypeError => detail\n    Puppet.log_exception(detail, _(\"Could not save %{json} %{request}: %{detail}\") % { json: name, request: request.key, detail: detail })\n  end\n\n  def destroy(request)\n    Puppet::FileSystem.unlink(path(request.key))\n  rescue => detail\n    unless detail.is_a? Errno::ENOENT\n      raise Puppet::Error, _(\"Could not destroy %{json} %{request}: %{detail}\") % { json: name, request: request.key, detail: detail }, detail.backtrace\n    end\n\n    1 # emulate success...\n  end\n\n  def search(request)\n    Dir.glob(path(request.key)).collect do |file|\n      load_json_from_file(file, request.key)\n    end\n  end\n\n  # Return the path to a given node's file.\n  def path(name, ext = '.json')\n    if name =~ Puppet::Indirector::BadNameRegexp then\n      Puppet.crit(_(\"directory traversal detected in %{json}: %{name}\") % { json: self.class, name: name.inspect })\n      raise ArgumentError, _(\"invalid key\")\n    end\n","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/indirector/json.rb#L10-L46","documentation":"The JSON terminus destroy calls Puppet::FileSystem.unlink on the stored file. Errno::ENOENT is swallowed and 1 is returned to emulate success, but any other error - EACCES/EPERM, EISDIR, read-only filesystem, EROFS - is re-raised as Puppet::Error 'Could not destroy json <key>: <original message>' with the original backtrace preserved.","triggerScenarios":"Destroying an entry in a json-backed indirection store when server_datadir (or client_datadir) files are owned by root while the service runs as puppet; the datadir lives on a read-only mount; the path points at a directory instead of a file.","commonSituations":"Master data dirs chowned incorrectly after package upgrades; runs mixed between root and the service user; SELinux denials; containerized puppetservers with read-only persistent volumes.","solutions":["Read the <detail> suffix; it names the real errno (permission denied, read-only file system, ...).","Fix ownership: chown -R <serviceuser>:<servicegroup> \"$(puppet config print server_datadir)\" (and client_datadir on agents where relevant).","Remount writable or relocate the datadir if the filesystem is read-only.","If SELinux is enforcing, check audit logs and run restorecon on the datadir.","Remove a stray directory-at-path manually if a path-type clash caused the error."],"exampleFix":"# before: service user cannot unlink root-owned store file\n# Could not destroy json node.example.com: Permission denied @ unlink_internal\n\n# after\nchown -R puppet:puppet /opt/puppetlabs/server/data/puppet\nsystemctl restart puppetserver","handlingStrategy":"try-catch","validationCode":"path = File.join(store_dir, indirection_name, key + '.json')\nraise ArgumentError, 'not removable by this user' unless File.writable?(File.dirname(path)) && (File.writable?(path) || !File.exist?(path))","typeGuard":null,"tryCatchPattern":"begin\n  indirection.destroy(key)\nrescue Puppet::Error => e\n  Puppet.err('destroy failed: ' + e.message) # operator follow-up: fix ownership\nend","preventionTips":["Run the puppet service under one user; never mix root and service-user writes to datadirs.","Declare datadir ownership in provisioning code.","Exercise destroy paths in smoke tests so permission drift surfaces early."],"tags":["puppet","json-store","filesystem","permissions","destroy"],"backgroundTag":"file-permission-denied","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}