{"record":{"id":"602328ddde1e1bd7","repo":"puppetlabs/puppet","slug":"the-indirection-name-must-be-purely-alphanumeric","errorCode":null,"errorMessage":"The indirection name must be purely alphanumeric, not '%{indirection_name}'","messagePattern":"The indirection name must be purely alphanumeric, not '%(.+?)'","errorType":"http","errorClass":"Puppet::Network::HTTP::Error::HTTPBadRequestError","httpStatus":400,"severity":"error","filePath":"lib/puppet/network/http/api/indirected_routes.rb","lineNumber":65,"sourceCode":"      trusted_information: Puppet::Context::TrustedInformation.remote(params[:authenticated], params[:node], certificate),\n    }\n    if params[:environment]\n      overrides[:current_environment] = params[:environment]\n    end\n\n    Puppet.override(overrides) do\n      send(\"do_#{method}\", indirection, key, params, request, response)\n    end\n  end\n\n  def uri2indirection(http_method, uri, params)\n    # the first field is always nil because of the leading slash,\n    indirection_type, version, indirection_name, key = uri.split(\"/\", 5)[1..]\n    url_prefix = \"/#{indirection_type}/#{version}\"\n    environment = params.delete(:environment)\n\n    if indirection_name !~ /^\\w+$/\n      raise Puppet::Network::HTTP::Error::HTTPBadRequestError, _(\"The indirection name must be purely alphanumeric, not '%{indirection_name}'\") % { indirection_name: indirection_name }\n    end\n\n    # this also depluralizes the indirection_name if it is a search\n    method = indirection_method(http_method, indirection_name)\n\n    # check whether this indirection matches the prefix and version in the\n    # request\n    if url_prefix != IndirectionType.url_prefix_for(indirection_name)\n      raise Puppet::Network::HTTP::Error::HTTPBadRequestError, _(\"Indirection '%{indirection_name}' does not match url prefix '%{url_prefix}'\") % { indirection_name: indirection_name, url_prefix: url_prefix }\n    end\n\n    indirection = Puppet::Indirector::Indirection.instance(indirection_name.to_sym)\n    unless indirection\n      raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new(\n        _(\"Could not find indirection '%{indirection_name}'\") % { indirection_name: indirection_name },\n        Puppet::Network::HTTP::Issues::HANDLER_NOT_FOUND\n      )\n    end","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/network/http/api/indirected_routes.rb#L47-L83","documentation":"Puppet's HTTP layer routes requests as /:prefix/:version/:indirection/:key (the URI is split on '/' and the third segment is the indirection name, e.g. 'catalog' or 'facts'). Before any lookup, uri2indirection requires that segment to match /^\\w+$/ — letters, digits and underscore only. A segment containing dashes, dots or spaces (encoded or not), or a segment shifted by double slashes, yields HTTP 400 with this message rather than a 404.","triggerScenarios":"Hand-crafted requests such as GET /puppet/v3/facts%20host/web1, /puppet/v3/node-report/x (dash), or //puppet/v3/catalog/web1 where the split leaves a malformed third field; any URL builder that interpolates user data into the indirection position.","commonSituations":"Custom scripts or curl calls with wrong URL layout; proxies and rewrite rules mangling the path (encoding, duplicate slashes); confusion about segment order; client libraries targeting a different REST layout than the server's.","solutions":["Build URLs in the documented order /:prefix/:version/:indirection/:key — e.g. GET /puppet/v3/facts/web1.example.com — URL-encoding only the key.","Use literal indirection names: catalog, facts, node, report, file_content, file_metadata, status.","Inspect the actual request path at the server (debug logging) for double slashes or percent-encoding introduced by proxies.","Prefer Puppet's own client/indirection APIs over hand-rolled HTTP."],"exampleFix":"# before\nGET /puppet/v3/facts%20host/web1.example.com   → 400, indirection must be alphanumeric\n\n# after\nGET /puppet/v3/facts/web1.example.com","handlingStrategy":"validation","validationCode":"INDIRECTIONS = %w[catalog facts node report file_content file_metadata status].freeze\nindirection = path.split('/', 5)[3]\nraise ArgumentError, 'malformed indirection' unless indirection =~ /^\\w+$/ && INDIRECTIONS.include?(indirection)\nresponse = client.get('/puppet/v3/' + indirection + '/' + ERB::Util.url_encode(key))","typeGuard":null,"tryCatchPattern":"resp = client.get(path)\ncase resp\nwhen Net::HTTPBadRequest\n  # 400 'indirection name must be purely alphanumeric' → fix the URL template; do not retry\n  raise \"malformed REST path: #{path}\"\nend","preventionTips":["Never build the indirection segment from user input; only the key varies per request.","Keep path templates in one place and assert the segment matches /\\w+/ before sending.","Watch proxy rewrite rules that percent-encode or collapse slashes.","Smoke-test custom clients against the documented v3 endpoints."],"tags":["puppet","ruby","rest-api","http","routing","bad-request"],"backgroundTag":"invalid-url-path-segment","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}