bblimke/webmock · error · WebMock::Response::InvalidBody

must be one of: #{valid_types}, but you've used a #{@body.cl

Error message

must be one of: #{valid_types}, but you've used a #{@body.class}. Please convert it by calling .to_json .to_xml, or otherwise convert it to a string.

What it means

WebMock::Response#body= validates the body of a stubbed response: only Proc, IO, Pathname, String and Array (plus nil) are accepted. A Hash is deliberately rejected with this message from lib/webmock/response.rb:121 because webmock will not guess your serialization - a Hash is ambiguous between JSON, XML and form-encoding. Note the asymmetry that trips developers: on the request side with(body: {...}) accepts hashes and compares them structurally, but the response side requires an already-serialized string.

Source

Thrown at lib/webmock/response.rb:121

    end

    private

    def stringify_body!
      if @body.is_a?(IO) || @body.is_a?(Pathname)
        io = @body
        @body = io.read
        io.close if io.respond_to?(:close)
      end
    end

    def assert_valid_body!
      valid_types = [Proc, IO, Pathname, String, Array]
      return if @body.nil?
      return if valid_types.any? { |c| @body.is_a?(c) }

      if @body.is_a?(Hash)
        raise InvalidBody, "must be one of: #{valid_types}, but you've used a #{@body.class}. " \
          "Please convert it by calling .to_json .to_xml, or otherwise convert it to a string."
      else
        raise InvalidBody, "must be one of: #{valid_types}. '#{@body.class}' given."
      end
    end

    def read_raw_response(io)
      socket = ::Net::BufferedIO.new(io)
      response = ::Net::HTTPResponse.read_new(socket)
      transfer_encoding = response.delete('transfer-encoding') #chunks were already read by curl
      response.reading_body(socket, true) {}

      options = {}
      options[:headers] = {}
      response.each_header {|name, value| options[:headers][name] = value}
      options[:headers]['transfer-encoding'] = transfer_encoding if transfer_encoding
      options[:body] = response.read_body
      options[:status] = [response.code.to_i, response.message]

View on GitHub (pinned to b187df8827)

Solutions

  1. Serialize it yourself: .to_return(body: { error: 'not found' }.to_json, headers: { 'Content-Type' => 'application/json' })
  2. Or switch to the JSON helper: .to_return_json(body: { error: 'not found' }) - it serializes and sets the content type
  3. If the hash is actually form data, use .to_return(body: URI.encode_www_form(hash))

Example fix

# before
stub_request(:get, 'https://api.example.com/me').to_return(body: { id: 1, name: 'Ada' })  # WebMock::InvalidBody

# after
stub_request(:get, 'https://api.example.com/me').to_return_json(body: { id: 1, name: 'Ada' })
Defensive patterns

Strategy: type-guard

Validate before calling

payload = { id: 1 }
stub.to_return(body: payload.respond_to?(:to_hash) ? payload.to_json : payload)

Type guard

def stub_body_valid?(b)
  b.nil? || [Proc, IO, Pathname, String, Array].any? { |c| b.is_a?(c) }
end

Try / catch

begin
  stub.to_return(body: payload)
rescue WebMock::InvalidBody
  stub.to_return_json(body: payload)  # payload is a Hash: serialize it
end

Prevention

When it happens

Trigger: stub_request(:any, url).to_return(body: { error: 'not found' }, status: 404). WebMock::Response.new(body: { ok: true }). A helper that builds a payload hash and passes it straight through as the response body.

Common situations: JSON API stubbing where the payload is naturally authored as a Ruby hash; refactoring specs from string bodies to structured ones; copy-pasting a with(body:) hash into to_return(body:).

Related errors


AI-assisted analysis of bblimke/webmock@b187df8827 (2026-08-23). Data as JSON: /api/errors/27a1e0066c1b2a63. Report an issue: GitHub.