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
- Serialize it yourself: .to_return(body: { error: 'not found' }.to_json, headers: { 'Content-Type' => 'application/json' })
- Or switch to the JSON helper: .to_return_json(body: { error: 'not found' }) - it serializes and sets the content type
- 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
- Use to_return_json for hash payloads
- Serialize in a payload factory so specs only ever see strings
- Remember with(body:) accepts hashes but to_return(body:) does not
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
- #to_return_json does not support passing a block
- must be one of: #{valid_types}. '#{@body.class}' given.
- Invalid JSON string: #{yaml}, Error: #{e.inspect}
- Real HTTP connections are disabled. Unregistered request: #{
- #with method invoked with no arguments. Either options hash
AI-assisted analysis of bblimke/webmock@b187df8827 (2026-08-23).
Data as JSON: /api/errors/27a1e0066c1b2a63.
Report an issue: GitHub.