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

must be one of: #{valid_types}. '#{@body.class}' given.

Error message

must be one of: #{valid_types}. '#{@body.class}' given.

What it means

The same response-body validator (lib/webmock/response.rb:124), non-Hash branch: the body is not nil, not one of Proc/IO/Pathname/String/Array, and not a Hash - e.g. Integer, Float, Symbol, true/false, OpenStruct or an arbitrary Struct - so it raises WebMock::InvalidBody listing the accepted classes. Unlike the Hash case there is no serialization hint, because there is no obvious encoding for the value; the fix is always to convert it to a string (or stream it via IO).

Source

Thrown at lib/webmock/response.rb:124

    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]
      options
    ensure
      socket.close

View on GitHub (pinned to b187df8827)

Solutions

  1. Convert to a string: .to_return(body: 42.to_s) - or body: 'true' / body: 'ok'
  2. If the endpoint is JSON, use .to_return_json(body: { count: 42 })
  3. For file or streaming responses, pass a File/IO or Pathname - those are supported as-is

Example fix

# before
stub_request(:get, 'https://api.example.com/count').to_return(body: 42)  # WebMock::InvalidBody: 'Integer' given

# after
stub_request(:get, 'https://api.example.com/count').to_return(body: '42')
Defensive patterns

Strategy: type-guard

Validate before calling

body = [Proc, IO, Pathname, String, Array].any? { |c| raw.is_a?(c) } || raw.nil? ? raw : raw.to_s
stub.to_return(body: body)

Type guard

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

Try / catch

begin
  stub.to_return(body: raw)
rescue WebMock::InvalidBody
  stub.to_return(body: raw.to_s)
end

Prevention

When it happens

Trigger: .to_return(body: 42) for a counter endpoint; .to_return(body: :ok); passing an OpenStruct or Struct built by a fixture factory; passing a boolean such as body: result.present?.

Common situations: Numeric or text responses passed as native types from test data; fixture builders returning Structs instead of strings; quick one-off stubs using whatever value the test computed.

Related errors


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