jnunemaker/httparty · error · ArgumentError

:query must be hash if using HTTP Post

Error message

:query must be hash if using HTTP Post

What it means

Request#validate raises ArgumentError ':query must be hash if using HTTP Post' when a POST request carries a non-nil options[:query] that does not respond to #to_hash. For POST you normally want `body:`; the `query:` option exists for appending URL parameters and, on POST specifically, httparty requires it to be a Hash so it can be normalized into the query string unambiguously. GET/PUT/PATCH with a string query are not subject to this check.

Source

Thrown at lib/httparty/request.rb:421

    end

    # Uses the HTTP Content-Type header to determine the format of the
    # response It compares the MIME type returned to the types stored in the
    # SupportedFormats hash
    def format_from_mimetype(mimetype)
      if mimetype && parser.respond_to?(:format_from_mimetype)
        parser.format_from_mimetype(mimetype)
      end
    end

    def validate
      raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0
      raise ArgumentError, 'only get, post, patch, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
      raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].respond_to?(:to_hash)
      raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth]
      raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].respond_to?(:to_hash)
      raise ArgumentError, ':digest_auth must be a hash' if options[:digest_auth] && !options[:digest_auth].respond_to?(:to_hash)
      raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].respond_to?(:to_hash)
    end

    def post?
      Net::HTTP::Post == http_method
    end

    def set_basic_auth_from_uri
      if path.userinfo
        username, password = path.userinfo.split(':')
        options[:basic_auth] = {username: username, password: password}
        @credentials_sent = true
      end
    end

    def decompress(body, encoding)
      Decompressor.new(body, encoding).decompress
    end

View on GitHub (pinned to 8f4a09e343)

Solutions

  1. Pass a Hash: `Foo.post(url, query: { a: 1, b: 2 })`.
  2. Parse an existing string: `query: Hash[URI.decode_www_form(str)]`.
  3. If the string is the payload, move it to `body:` instead of `query:`.

Example fix

# before
Foo.post('http://x/search', query: 'q=ruby&page=2')   # String on POST -> ArgumentError

# after
Foo.post('http://x/search', query: { q: 'ruby', page: 2 })
Defensive patterns

Strategy: validation

Validate before calling

if query && !query.respond_to?(:to_hash)
  query = Hash[URI.decode_www_form(query)]  # or move it to body:
end
Foo.post(url, query: query)

Type guard

hash_like = ->(v) { v.respond_to?(:to_hash) }

Try / catch

begin
  Foo.post(url, query: q)
rescue ArgumentError => e
  raise unless e.message.include?(':query')
  Foo.post(url, query: Hash[URI.decode_www_form(q)])
end

Prevention

When it happens

Trigger: `Foo.post(url, query: 'a=1&b=2')`, `Foo.post(url, query: URI.decode_www_form(...))` (array of pairs), or copy-pasting a pre-built query string that worked with `get` into a `post` call.

Common situations: Moving a search-pagination call from GET to POST while keeping the query-string style, paginating API wrappers that pass through user-supplied query strings, and helper methods shared across verbs.

Understand the failure class

Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.

Related errors


AI-assisted analysis of jnunemaker/httparty@8f4a09e343 (2026-08-21). Data as JSON: /api/errors/312856dd4d4c01f5. Report an issue: GitHub.