WireMock.Net icon indicating copy to clipboard operation
WireMock.Net copied to clipboard

MimePartMatcher does not match multipart/form-data request body (possible bug?)

Open BrunoBeraud opened this issue 2 months ago • 2 comments

Hello, First of all, thank you for this library — it helps us a lot in our development work!

I'm using WireMock.Net.Testcontainers 1.15.0 for our tests and the sheyenrath/wiremock.net-alpine:1.15.0 Docker image in our development environment. We want to define a stub endpoint that accepts a multipart/form-data HTTP request.

Unfortunately, we’re not able to create a request body matcher using MimePartMatcher in a JSON mapping file.


What Works

This body matcher works correctly:

"Body": {
  "Matcher": {
    "Name": "RegexMatcher",
    "Pattern": ".+"
  }
}

What Doesn’t Work

When using MimePartMatcher, even with very lenient matchers, the matcher always returns a 0.0 score for the body:

"Body": {
  "Matcher": {
    "Name": "MimePartMatcher",
    "ContentTypeMatcher": {
      "Name": "RegexMatcher",
      "Pattern": ".*"
    },
    "ContentDispositionMatcher": {
      "Name": "RegexMatcher",
      "Pattern": ".*"
    },
    "ContentMatcher": {
      "Name": "RegexMatcher",
      "Pattern": ".*"
    }
  }
}

I also tried:

  • WildcardMatcher with * for all matchers
  • JsonMatcher with the exact JSON sent
  • JsonPartialMatcher using regex (with "Regex": true) and .* for all JSON values
  • Setting "ContentTypeMatcher" with "Name": "ContentTypeMatcher" and "Pattern": "application/json"

→ Always a 0.0 score for BodyMatcher (while PathMatcher, MethodMatcher, and HeadersMatcher all score 1.0).


Request Example

Here’s a sample curl request (exported from Postman):

curl -L -X POST "http://localhost:8080/my-stub-endpoint" \
  -F "metadata=\"{\\\"ID\\\": \\\"9858013b-e020-4ef9-b8a8-0bebc740e6a7\\\", \\\"DATE\\\": \\\"2025-08-15T13:45:30.0000000Z\\\", \\\"NAME\\\": \\\"32c9a8dd-e214-4afb-9611-9cde81f827c6\\\", \\\"NUMBER\\\": 10}\";type=application/json"

Observation

From the /__admin/requests endpoint, here’s an example of the request body WireMock receives:

"Headers": {
        "Accept": [
          "*/*"
        ],
        "Host": [
          "localhost:8080"
        ],
        "User-Agent": [
          "curl/8.13.0"
        ],
        "Content-Type": [
          "multipart/form-data; boundary=------------------------woli8b80pw4vBJtNpAMOKS"
        ],
        "Content-Length": [
          "334"
        ]
      },
"Body": "--------------------------woli8b80pw4vBJtNpAMOKS\r\n
Content-Disposition: form-data; name=\"metadata\"\r\n
Content-Type: application/json\r\n
\r\n
{\"ID\": \"9858013b-e020-4ef9-b8a8-0bebc740e6a7\", \"DATE\": \"2025-08-15T13:45:30.0000000Z\", \"NAME\": \"32c9a8dd-e214-4afb-9611-9cde81f827c6\", \"NUMBER\": 10}\r\n
--------------------------woli8b80pw4vBJtNpAMOKS--\r\n"

After digging a bit into the code, it seems that the issue comes from the use of the MimeKit library, which appears unable to parse this request body. As a result, WireMock fails to extract the body parts, and the matcher score stays at its default value (0.0).


Possible Root Cause

It looks like the body content doesn’t include headers (such as Content-Type with a boundary) that would allow MimeKit to properly parse it into BodyParts. In real HTTP requests, this header is not part of the body — it’s in the request headers — which might explain the mismatch compared to the examples in MimePartMatcherTests, where the content type is included in the body.


Question

Could this be a limitation or a bug in how MimePartMatcher / MimeKit handles real multipart/form-data requests? Or is there a recommended approach to match multipart requests in JSON mappings?

Thank you in advance for your time and for maintaining this great library!

BrunoBeraud avatar Oct 24 '25 08:10 BrunoBeraud

@BrunoBeraud I need to check.

StefH avatar Dec 07 '25 17:12 StefH

@BrunoBeraud I did check the code and it's correct, in my unit tests I need to pass that text (including Content-Type as part of the body) to get a valid MimeMessage with 3 parts.

The real code will use the Header and prepend this to your body.


I think that your mapping json is wrong. A possible mapping json for checking 3 parts (same as in the unit test) can be like:

{
    "Guid": "b9c82182-e469-41da-bcaf-b6e3157fefdb",
    "UpdatedAt": "2023-07-24T18:12:55.564978Z",
    "Request": {
        "Path": {
            "Matchers": [
                {
                    "Name": "WildcardMatcher",
                    "Pattern": "/multipart",
                    "IgnoreCase": false
                }
            ]
        },
        "Methods": [
            "POST"
        ],
        "Body": {
            "Matchers": [
                {
                    "ContentTypeMatcher": {
                        "Name": "ContentTypeMatcher",
                        "Pattern": "text/plain",
                        "IgnoreCase": false
                    },
                    "ContentMatcher": {
                        "Name": "ExactMatcher",
                        "Pattern": "This is some plain text",
                        "IgnoreCase": false
                    },
                    "Name": "MimePartMatcher"
                },
                {
                    "ContentTypeMatcher": {
                        "Name": "ContentTypeMatcher",
                        "Pattern": "text/json",
                        "IgnoreCase": false
                    },
                    "ContentMatcher": {
                        "Name": "JsonMatcher",
                        "Pattern": {
                            "Key": "Value"
                        },
                        "IgnoreCase": true
                    },
                    "Name": "MimePartMatcher"
                },
                {
                    "ContentTypeMatcher": {
                        "Name": "ContentTypeMatcher",
                        "Pattern": "image/png",
                        "IgnoreCase": true
                    },
                    "ContentDispositionMatcher": {
                        "Name": "ExactMatcher",
                        "Pattern": "attachment; filename=\"image.png\"",
                        "IgnoreCase": true
                    },
                    "ContentTransferEncodingMatcher": {
                        "Name": "ExactMatcher",
                        "Pattern": "base64",
                        "IgnoreCase": true
                    },
                    "ContentMatcher": {
                        "Name": "ExactObjectMatcher",
                        "Pattern": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjlAAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCC"
                    },
                    "Name": "MimePartMatcher"
                }
            ],
            "MatchOperator": "Or"
        }
    },
    "Response": {
        "BodyDestination": "SameAsSource",
        "Body": "MultiPart is ok"
    }
}

See also this extra unit test --> https://github.com/wiremock/WireMock.Net/pull/1389

StefH avatar Dec 08 '25 19:12 StefH

Hello, I have tried your json multipart request mapping file with sheyenrath/wiremock.net-alpine:1.15.0 docker container. I used this curl request: curl -X POST http://localhost:8080/multipart \ -F 'plainText=This is some plain text;type=text/plain' \ -F 'jsonData={ "Key": "Value" };type=text/json' \ -F '[email protected];type=image/png' The image is the one expecting in the ExactObjectMatcher base64 pattern

MatchDetails all 1.0 but BodyMatcher still 0.0...

BrunoBeraud avatar Dec 17 '25 15:12 BrunoBeraud

@BrunoBeraud

I did not test the Docker image, but just local code. And the mapping in json is like:

{
    "Guid": "b9c82182-e469-41da-bcaf-b6e3157fefdb",
    "UpdatedAt": "2025-12-18T17:21:57.3879723Z",
    "Request": {
        "Path": {
            "Matchers": [
                {
                    "Name": "WildcardMatcher",
                    "Pattern": "/multipart",
                    "IgnoreCase": false
                }
            ]
        },
        "Methods": [
            "POST"
        ],
        "Body": {
            "Matchers": [
                {
                    "Name": "MimePartMatcher",
                    "ContentTypeMatcher": {
                        "Name": "ContentTypeMatcher",
                        "Pattern": "text/plain",
                        "IgnoreCase": false
                    },
                    "ContentMatcher": {
                        "Name": "ExactMatcher",
                        "Pattern": "This is some plain text",
                        "IgnoreCase": false
                    }
                },
                {
                    "Name": "MimePartMatcher",
                    "ContentTypeMatcher": {
                        "Name": "ContentTypeMatcher",
                        "Pattern": "text/json",
                        "IgnoreCase": false
                    },
                    "ContentMatcher": {
                        "Name": "JsonMatcher",
                        "Pattern": {
                            "Key": "Value"
                        },
                        "IgnoreCase": true,
                        "Regex": false
                    }
                },
                {
                    "Name": "MimePartMatcher",
                    "ContentTypeMatcher": {
                        "Name": "ContentTypeMatcher",
                        "Pattern": "image/png",
                        "IgnoreCase": false
                    },
                    "ContentDispositionMatcher": {
                        "Name": "ExactMatcher",
                        "Pattern": "attachment; filename=\"image.png\"",
                        "IgnoreCase": false
                    },
                    "ContentTransferEncodingMatcher": {
                        "Name": "ExactMatcher",
                        "Pattern": "base64",
                        "IgnoreCase": false
                    },
                    "ContentMatcher": {
                        "Name": "ExactObjectMatcher",
                        "Pattern": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjlAAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCC"
                    }
                }
            ],
            "MatchOperator": "Or"
        }
    },
    "Response": {
        "BodyDestination": "SameAsSource",
        "Body": "MultiPart is ok"
    }
}

The same mapping in code is:

var textPlainContentTypeMatcher = new ContentTypeMatcher("text/plain");
var textPlainContentMatcher = new ExactMatcher("This is some plain text");
var textPlainMatcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, textPlainContentTypeMatcher, null, null, textPlainContentMatcher);

var textJsonContentTypeMatcher = new ContentTypeMatcher("text/json");
var textJsonContentMatcher = new JsonMatcher(new { Key = "Value" }, true);
var textJsonMatcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, textJsonContentTypeMatcher, null, null, textJsonContentMatcher);

var imagePngContentTypeMatcher = new ContentTypeMatcher("image/png");
var imagePngContentDispositionMatcher = new ExactMatcher("attachment; filename=\"image.png\"");
var imagePngContentTransferEncodingMatcher = new ExactMatcher("base64");
var imagePngContentMatcher = new ExactObjectMatcher(Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjlAAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCC"));
var imagePngMatcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, imagePngContentTypeMatcher, imagePngContentDispositionMatcher, imagePngContentTransferEncodingMatcher, imagePngContentMatcher);

var matchers = new IMatcher[]
{
    textPlainMatcher,
    textJsonMatcher,
    imagePngMatcher
};

server
    .Given(Request.Create()
        .WithPath("/multipart")
        .UsingPost()
        .WithMultiPart(matchers)
    )
    .WithGuid("b9c82182-e469-41da-bcaf-b6e3157fefdb")
    .RespondWith(Response.Create()
        .WithBody("MultiPart is ok")
    );

See also: https://github.com/wiremock/WireMock.Net/blob/mime-part-matcher/examples/WireMock.Net.Console.NET8/MainApp.cs#L559


The image.png is attached to this comment. (It's a small red dot below...) Image


Running CURL on windows:

PS C:\temp> curl -X POST http://localhost:9091/multipart \ -F 'plainText=This is some plain text;type=text/plain' \ -F 'jsonData={ "Key": "Value" };type=text/json' \ -F '[email protected];type=image/png'
MultiPart is okcurl: (3) URL rejected: Bad hostname
curl: (3) URL rejected: Bad hostname
curl: (3) URL rejected: Bad hostname
PS C:\temp>

Logging:

18/12/2025 17:30:39 [DebugRequestResponse] : Admin[False] {
  "Guid": "950acde0-f0eb-4810-ba07-022e7305c39e",
  "Request": {
    "ClientIP": "::1",
    "DateTime": "2025-12-18T17:30:37.2241528Z",
    "Path": "/multipart",
    "AbsolutePath": "/multipart",
    "Url": "http://localhost:9091/multipart",
    "AbsoluteUrl": "http://localhost:9091/multipart",
    "ProxyUrl": null,
    "Query": {},
    "Method": "POST",
    "HttpVersion": "1.1",
    "Headers": {
      "Accept": [
        "*/*"
      ],
      "Host": [
        "localhost:9091"
      ],
      "User-Agent": [
        "curl/8.16.0"
      ],
      "Content-Type": [
        "multipart/form-data; boundary=------------------------5kocnZUaqTqJbcOKOCaG3m"
      ],
      "Content-Length": [
        "591"
      ]
    },
    "Cookies": {},
    "Body": null,
    "BodyAsJson": null,
    "BodyAsBytes": "LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS01a29jblpVYXFUcUpiY09LT0NhRzNtDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9InBsYWluVGV4dCINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpUaGlzIGlzIHNvbWUgcGxhaW4gdGV4dA0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS01a29jblpVYXFUcUpiY09LT0NhRzNtDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9Impzb25EYXRhIg0KQ29udGVudC1UeXBlOiB0ZXh0L2pzb24NCg0KeyAiS2V5IjogIlZhbHVlIiB9DQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTVrb2NuWlVhcVRxSmJjT0tPQ2FHM20NCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0iaW1hZ2UiOyBmaWxlbmFtZT0iaW1hZ2UucG5nIg0KQ29udGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjlAAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCCDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTVrb2NuWlVhcVRxSmJjT0tPQ2FHM20tLQ0K",
    "BodyEncoding": null,
    "DetectedBodyType": "Bytes",
    "DetectedBodyTypeFromContentType": "MultiPart"
  },
  "Response": {
    "StatusCode": null,
    "Headers": {},
    "BodyDestination": "SameAsSource",
    "Body": "MultiPart is ok",
    "BodyAsJson": null,
    "BodyAsBytes": null,
    "BodyAsFile": null,
    "BodyAsFileIsCached": null,
    "BodyOriginal": null,
    "BodyEncoding": {
      "CodePage": 65001,
      "EncodingName": "Unicode (UTF-8)",
      "WebName": "utf-8"
    },
    "DetectedBodyType": 1,
    "DetectedBodyTypeFromContentType": null,
    "FaultType": null,
    "FaultPercentage": null
  },
  "MappingGuid": "b9c82182-e469-41da-bcaf-b6e3157fefdb",
  "MappingTitle": null,
  "RequestMatchResult": {
    "TotalScore": 3.0,
    "TotalNumber": 3,
    "IsPerfectMatch": true,
    "AverageTotalScore": 1.0,
    "MatchDetails": [
      {
        "Name": "PathMatcher",
        "Score": 1.0
      },
      {
        "Name": "MethodMatcher",
        "Score": 1.0
      },
      {
        "Name": "MultiPartMatcher",
        "Score": 1.0
      }
    ]
  },
  "PartialMappingGuid": "b9c82182-e469-41da-bcaf-b6e3157fefdb",
  "PartialMappingTitle": null,
  "PartialRequestMatchResult": {
    "TotalScore": 3.0,
    "TotalNumber": 3,
    "IsPerfectMatch": true,
    "AverageTotalScore": 1.0,
    "MatchDetails": [
      {
        "Name": "PathMatcher",
        "Score": 1.0
      },
      {
        "Name": "MethodMatcher",
        "Score": 1.0
      },
      {
        "Name": "MultiPartMatcher",
        "Score": 1.0
      }
    ]
  }
}

StefH avatar Dec 18 '25 17:12 StefH