bblimke/webmock · error · ArgumentError
WebMock does not support matching body for multipart/form-da
Error message
WebMock does not support matching body for multipart/form-data requests yet :(
What it means
When a stub or assertion narrows on body:, BodyPattern#matches? first inspects the incoming request's Content-Type; if it starts with multipart/form-data it raises ArgumentError from lib/webmock/request_pattern.rb:324 because webmock has no multipart parser to turn the body into a comparable hash. It fires at match time - the moment a multipart request is actually made (or verified) against a stub that carries a body constraint - not when the stub is declared. Dropping the body constraint avoids the check entirely.
Source
Thrown at lib/webmock/request_pattern.rb:326
when :json then
WebMock::Util::Parsers::JSON.parse(body)
when :xml then
WebMock::Util::Parsers::XML.parse(body)
else
WebMock::Util::QueryMapper.query_to_values(body, notation: Config.instance.query_values_notation)
end
rescue WebMock::Util::Parsers::ParseError
nil
end
def body_format(content_type)
normalized_content_type = content_type.sub(/\A(application\/)[a-zA-Z0-9.-]+\+(json|xml)\Z/,'\1\2')
BODY_FORMATS[normalized_content_type]
end
def assert_non_multipart_body(content_type)
if content_type =~ %r{^multipart/form-data}
raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
end
end
# Compare two hashes for equality
#
# For two hashes to match they must have the same length and all
# values must match when compared using `#===`.
#
# The following hashes are examples of matches:
#
# {a: /\d+/} and {a: '123'}
#
# {a: '123'} and {a: '123'}
#
# {a: {b: /\d+/}} and {a: {b: '123'}}
#
# {a: {b: 'wow'}} and {a: {b: 'wow'}}
#View on GitHub (pinned to b187df8827)
Solutions
- Drop the body constraint for multipart stubs and match method + URI (+ headers) only: stub_request(:post, url).to_return(status: 201)
- Assert the body through a with block, which bypasses BodyPattern: .with { |req| req.body.to_s.include?('avatar.png') }
- Inspect the uploaded parts after the call with WebMock.after_request { |request_signature, _response| ... }, reading request_signature.body
- If you control the client, make the test send urlencoded (no file) so body: hash matching works
Example fix
# before
stub_request(:post, 'https://api.example.com/uploads')
.with(body: { title: 'avatar' }) # ArgumentError once the client posts multipart
RestClient.post 'https://api.example.com/uploads', { title: 'avatar', file: File.new('a.png') }
# after
stub_request(:post, 'https://api.example.com/uploads').to_return(status: 201)
RestClient.post 'https://api.example.com/uploads', { title: 'avatar', file: File.new('a.png') } Defensive patterns
Strategy: validation
Validate before calling
# Choose the stub shape from the payload: files force multipart, which must not use body: def stub_upload(url, fields) stub = stub_request(:post, url) fields.key?(:file) ? stub : stub.with(body: fields) end
Try / catch
begin
assert_requested(:post, url, body: { title: 'avatar' })
rescue ArgumentError
skip 'webmock cannot match multipart bodies; use a with-block assertion instead'
end Prevention
- Never pair body: with clients that send multipart/form-data
- Match uploads by URI and inspect bodies in after_request callbacks
- Encapsulate stub creation in a helper that knows whether the payload contains files
When it happens
Trigger: stub_request(:post, url).with(body: { name: 'x' }) combined with code posting multipart: Faraday with faraday-multipart middleware, RestClient.post(url, file: File.new(...)), HTTParty or Typhoeus attaching a file, or any form helper switching to multipart once a file field exists. Also assert_requested(:post, url, body: {...}) verified against a multipart request.
Common situations: File-upload specs that started as plain urlencoded form posts (where body: hash matching works) and gained a file attachment later; third-party API clients that always send multipart; a request-library upgrade changing the default encoding.
Related errors
- #with method invoked with no arguments. Either options hash
- The basic_auth option value should be an array which contain
- Real HTTP connections are disabled. Unregistered request: #{
- URI should be a String, Regexp, Addressable::Template, a cal
- #to_return_json does not support passing a block
AI-assisted analysis of bblimke/webmock@b187df8827 (2026-08-23).
Data as JSON: /api/errors/655efd82c878ca70.
Report an issue: GitHub.