Matching multipart request bodies with VCR

Martijn

Martijn Versluis – 20 August 2019
306 words in about 2 minutes

When using VCR to test a HTTP multipart request, you might experience mismatching cassettes because of multipart boundaries. Multipart boundaries are most often randomly generated by the HTTP library or browser, so when VCR tries to match a multipart request with a cassette, the boundaries will be different and the match will fail.

However, it is quite simple to replace the random boundaries by fixed ones before matching requests. A multipart request will have a Content-Type header, which not only makes the request recognizable as multipart request but also specifies the exact boundary that is used. In this case I took a cassette from a Ruby project, so the Content-Type looks like this:

1
multipart/form-data; boundary=----RubyFormBoundaryTsqIBL0iujC5POpr

Given this header we should be able to simply replace the boundaries with a fixed value. VCR has a configuration option called match_request_on, which is an array that accepts symbols for predefined request matchers (see https://relishapp.com/vcr/vcr/v/5-0-0/docs/request-matching) or a callable (Proc, Lambda). I’m not a real fan of using procs for this but as long as we create an object that responds to call we will be fine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class VCRMultipartBodyMatcher
  MULTIPART_HEADER_MATCHER = %r{^multipart/form-data; boundary=(.+)$}
  
  def call(request_1, request_2)
    normalized_multipart_body(request_1) == normalized_multipart_body(request_2)
  end

  private

  def normalized_multipart_body(request)
    content_type = (request.headers['Content-Type'] || []).first.to_s
  
    return request.body unless multipart_request?(content_type)
  
    boundary = MULTIPART_HEADER_MATCHER.match(content_type)[1]
    request.body.gsub(boundary, '----RubyFormBoundaryTsqIBL0iujC5POpr')
  end
  
  def multipart_request?(content_type)
    return false if content_type.empty?

    MULTIPART_HEADER_MATCHER.match?(content_type)
  end
end

Now we can tell VCR to use our custom matcher:

1
2
3
4
5
VCR.configure do |config|
  config.default_cassette_options = {
    match_requests_on: [:method, :uri, VCRMultipartBodyMatcher.new]
  }
end

Now, when running the test the boundaries of the request and the cassette will be equal, so the cassette will only mismatch if the request parameters differ. 👍

Do you need help with testing your application? At Kabisa we know a lot about writing good tests. Leave us a message if you would like to get in touch.

At Kabisa, privacy is of the greatest importance. We think it is important that the data our visitors leave behind is handled with care. For example, you will not find tracking cookies from third parties such as Facebook, Hotjar or Hubspot on our website. Only cookies from Google and Vimeo are used in order to improve the user experience of our visitors. These cookies also ensure that relevant advertisements are displayed. Read more about the use of cookies in our privacy statement.