puppetlabs/puppet · error · ArgumentError

'post' requires a 'content-type' header

Error message

'post' requires a 'content-type' header

What it means

Puppet::HTTP::Client#post requires a Content-Type header: default client headers (User-Agent, Accept, etc.) do not include one, so a request whose merged headers lack it raises ArgumentError just before send. The check reads request['Content-Type'], which is case-sensitive.

Source

Thrown at lib/puppet/http/client.rb:276

  #   as the `body` argument.
  # @param [Hash] params encoded and set as the url query
  # @!macro request_options
  #
  # @yield [Puppet::HTTP::Response] if a block is given yields the response
  #
  # @return [Puppet::HTTP::Response] the response
  #
  # @api public
  def post(url, body, headers: {}, params: {}, options: {}, &block)
    raise ArgumentError, "'post' requires a string 'body' argument" unless body.is_a?(String)

    url = encode_query(url, params)

    request = Net::HTTP::Post.new(url, @default_headers.merge(headers))
    request.body = body
    request.content_length = body.bytesize

    raise ArgumentError, "'post' requires a 'content-type' header" unless request['Content-Type']

    execute_streaming(request, options: options, &block)
  end

  # Submits a DELETE HTTP request to the given url.
  #
  # @param [URI] url the location to submit the http request
  # @param [Hash] headers merged with the default headers defined by the client
  # @param [Hash] params encoded and set as the url query
  # @!macro request_options
  #
  # @return [Puppet::HTTP::Response] the response
  #
  # @api public
  def delete(url, headers: {}, params: {}, options: {})
    url = encode_query(url, params)

    request = Net::HTTP::Delete.new(url, @default_headers.merge(headers))

View on GitHub (pinned to e227c27540)

Solutions

  1. Pass 'Content-Type' (exact casing, string key) with a value matching the body, e.g. 'application/json'.
  2. Use the service layer (Puppet::HTTP::Service::Report, ::Ca) which negotiates Content-Type itself.
  3. Centralize header construction in one helper so casing mistakes cannot diverge per call site.

Example fix

# before
client.post(url, JSON.generate(report), headers: { 'Accept' => 'application/json' })

# after
client.post(url, JSON.generate(report),
            headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' })
Defensive patterns

Strategy: validation

Validate before calling

headers = { 'Content-Type' => 'application/json' }.merge(headers)
client.post(url, body, headers: headers)

Type guard

has_content_type = ->(h) { h.any? { |k, _| k.is_a?(String) && k == 'Content-Type' } }
raise ArgumentError, 'Content-Type required' unless has_content_type.call(headers)

Prevention

When it happens

Trigger: client.post(url, body) with no headers argument; headers: { 'content-type' => 'application/json' } in lowercase; headers supplied with symbol keys :Content_Type that never reach the Net::HTTP request.

Common situations: Submitting reports or CA CSRs with hand-rolled calls where the old API defaulted the header; header hashes built from symbolized config keys; casing mismatches after refactoring shared header constants.

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/bb9a1e46521a6fd4. Report an issue: GitHub.