{"record":{"id":"2ed82bfdec25e6a1","repo":"puppetlabs/puppet","slug":"no-support-for-plurality-indirection-for-http","errorCode":null,"errorMessage":"No support for plurality %{indirection} for %{http_method} operations","messagePattern":"No support for plurality %(.+?) for %(.+?) operations","errorType":"http","errorClass":"Puppet::Network::HTTP::Error::HTTPBadRequestError","httpStatus":400,"severity":"error","filePath":"lib/puppet/network/http/api/indirected_routes.rb","lineNumber":244,"sourceCode":"        return model_class.convert_from(formatter.name.to_s, data)\n      rescue => e\n        raise Puppet::Network::HTTP::Error::HTTPBadRequestError, _(\"The request body is invalid: %{message}\") % { message: e.message }\n      end\n    end\n\n    # TRANSLATORS \"mime-type\" is a keyword and should not be translated\n    raise Puppet::Network::HTTP::Error::HTTPUnsupportedMediaTypeError.new(\n      _(\"Client sent a mime-type (%{header}) that doesn't correspond to a format we support\") % { header: request.headers['content-type'] },\n      Puppet::Network::HTTP::Issues::UNSUPPORTED_MEDIA_TYPE\n    )\n  end\n\n  def indirection_method(http_method, indirection)\n    raise Puppet::Network::HTTP::Error::HTTPMethodNotAllowedError, _(\"No support for http method %{http_method}\") % { http_method: http_method } unless METHOD_MAP[http_method]\n\n    method = METHOD_MAP[http_method][plurality(indirection)]\n    unless method\n      raise Puppet::Network::HTTP::Error::HTTPBadRequestError, _(\"No support for plurality %{indirection} for %{http_method} operations\") % { indirection: plurality(indirection), http_method: http_method }\n    end\n\n    method\n  end\n\n  def self.pluralize(indirection)\n    (indirection == \"status\" ? \"statuses\" : indirection + \"s\")\n  end\n  private_class_method :pluralize\n\n  def plurality(indirection)\n    # NOTE These specific hooks for paths are ridiculous, but it's a *many*-line\n    # fix to not need this, and our goal is to move away from the complication\n    # that leads to the fix being too long.\n    return :singular if indirection == \"facts\"\n    return :singular if indirection == \"status\"\n    return :singular if indirection == \"certificate_status\"\n","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/network/http/api/indirected_routes.rb#L226-L262","documentation":"After the verb check, the handler computes plurality(indirection) (plural for search-style paths such as facts or statuses) and looks up METHOD_MAP[http_method][plurality]. In this METHOD_MAP, GET supports both plural (search) and singular (find), while POST, PUT and DELETE define only singular entries. A verb applied to the wrong plurality therefore returns HTTP 400 naming the plurality and verb.","triggerScenarios":"PUT or POST to /puppet/v3/facts (facts is plural; submission must be PUT /puppet/v3/facts/<certname>); DELETE against /puppet/v3/statuses; any save/destroy attempt against a plural/search path.","commonSituations":"Developers applying generic REST conventions where POST targets a collection; custom fact submitters using POST on the collection URL; adapting examples from other REST APIs without checking METHOD_MAP.","solutions":["Save data against the singular form: PUT /puppet/v3/facts/<certname>","Use GET on plural paths only where METHOD_MAP defines :search (facts, statuses)","Read METHOD_MAP at the top of lib/puppet/network/http/api/indirected_routes.rb for the authoritative verb/plurality pairs"],"exampleFix":"# before: saving to the plural form\ncurl -X POST -H 'Content-Type: application/json' -d @facts.json \\\n  \"https://puppet:8140/puppet/v3/facts?environment=production\"\n\n# after: save via PUT on the singular form\ncurl -X PUT -H 'Content-Type: application/json' -d @facts.json \\\n  \"https://puppet:8140/puppet/v3/facts/mynode?environment=production\"","handlingStrategy":"validation","validationCode":"METHOD_MAP = {\n  'GET'    => { plural: :search, singular: :find },\n  'POST'   => { singular: :find },\n  'PUT'    => { singular: :save },\n  'DELETE' => { singular: :destroy }\n}.freeze\n\ndef verb_supported?(verb, plural)\n  !METHOD_MAP.dig(verb, plural ? :plural : :singular).nil?\nend\n\nraise ArgumentError, \"#{verb} not valid for #{'im' unless plural}plural form\" unless verb_supported?(verb, plural)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Memorize the pattern: reads are GET (plural=search, singular=find), saves are PUT on the singular form, deletes are singular","Write client helpers that encode the allowed verb/plurality pairs once"],"tags":["http","method-not-allowed","plurality","rest-api"],"backgroundTag":"http-method-not-allowed","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}