Provide the ability to specify the Content Type of a non-named Multipart Part
Is your feature request related to a problem? Please describe.
When utilising @RequestPart on a Controller argument in Spring for working with multipart/form-data requests consisting of e.g. a file part and a JSON part—and expecting the JSON part to be correctly deserialised to a POJO—it relies on the Content-Type of the request part to find and apply the relevant HttpMessagingConverter; defaulting to application/octet-stream if none was provided.
From the client perspective these parts are not files as such and do not have file names. MultipartBodyBuilder.part(String name, Object part, MediaType contentType) for example allows specifying such a part with a relevant content type when working with the WebClient.
Describe the solution you'd like
Provide the ability to specify the content type of a multipart part without a filename within a contract so as to generate valid requests.
Describe alternatives you've considered
Utilising the named(name, content, contentType) method, however this puts the burden onto any integrating party to provide a dummy name to satisfy the generated WireMock mappings as it is not optional whilst not being required by the endpoint.
Additional context
REST Assured seems to have various flavours of multiPart, some providing the ability to specify a multipart part with a relevant content type as desired which is probably of interest as compared to the currently used param method which seems more intended for form params.
In the process of attempting to provide a part(content, contentType) method or the like, but figured I'd open for discussion if this is something that has been covered previously or whether I have misunderstood any aspect of verifying a JSON encoded @RequestPart via Spring Cloud Contract.
Do you have a small Java-Maven sample that you could show with such client - server communication?
@marcingrzejszczak I have built a small example in https://github.com/adrianhj/spring-cloud-contract-1888, hopefully this helps showcase the gap (issue?) utilising Spring.
Areas of interest:
- The Controller being called: https://github.com/adrianhj/spring-cloud-contract-1888/blob/main/src/main/java/com/example/springcloudcontract1888/SpringCloudContract1888Application.java#L24
- A test showing the endpoint being called via RestTemplate (the Jackson backed HttpMessageConverter invoked by the RestTemplate when serializing the payload supplies the Content-Type internally, see log below): https://github.com/adrianhj/spring-cloud-contract-1888/blob/main/src/test/java/com/example/springcloudcontract1888/SpringCloudContract1888ApplicationTests.java#L33
- A failing contract test due to missing content type header on part with some comments for possible desired syntax/options: https://github.com/adrianhj/spring-cloud-contract-1888/blob/main/src/test/resources/contracts/example.groovy
- The test generated by the above but modified with desired output and now passing: https://github.com/adrianhj/spring-cloud-contract-1888/blob/main/src/test/java/com/example/springcloudcontract1888/DesiredContractVerifierTest.java#L14
For the RestTemplate backed test, I have upped the logging to easily see what is sent over the wire:
o.s.web.client.RestTemplate : HTTP POST http://localhost:57620/example
o.s.web.client.RestTemplate : Accept=[application/json, application/*+json]
o.s.web.client.RestTemplate : Writing [{file=[class path resource [contracts/file.txt]], metadata=[Metadata[value=example]]}] as "multipart/form-data"
org.apache.hc.client5.http.headers : http-outgoing-0 >> POST /example HTTP/1.1
org.apache.hc.client5.http.headers : http-outgoing-0 >> Accept: application/json, application/*+json
org.apache.hc.client5.http.headers : http-outgoing-0 >> Content-Type: multipart/form-data;boundary=nVCcViZpN810-A5mTBQ7AuGDfftezMFj_jPX
org.apache.hc.client5.http.headers : http-outgoing-0 >> Accept-Encoding: gzip, x-gzip, deflate
org.apache.hc.client5.http.headers : http-outgoing-0 >> Content-Length: 345
org.apache.hc.client5.http.headers : http-outgoing-0 >> Host: localhost:57620
org.apache.hc.client5.http.headers : http-outgoing-0 >> Connection: keep-alive
org.apache.hc.client5.http.headers : http-outgoing-0 >> User-Agent: Apache-HttpClient/5.1.4 (Java/17.0.6)
org.apache.hc.client5.http.wire : http-outgoing-0 >> "POST /example HTTP/1.1[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Accept: application/json, application/*+json[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Type: multipart/form-data;boundary=nVCcViZpN810-A5mTBQ7AuGDfftezMFj_jPX[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Accept-Encoding: gzip, x-gzip, deflate[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Length: 345[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Host: localhost:57620[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Connection: keep-alive[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "User-Agent: Apache-HttpClient/5.1.4 (Java/17.0.6)[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "--nVCcViZpN810-A5mTBQ7AuGDfftezMFj_jPX[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Disposition: form-data; name="file"; filename="file.txt"[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Type: text/plain[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Length: 4[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Test[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "--nVCcViZpN810-A5mTBQ7AuGDfftezMFj_jPX[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Disposition: form-data; name="metadata"[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Type: application/json[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "{"value":"example"}[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "--nVCcViZpN810-A5mTBQ7AuGDfftezMFj_jPX--[\r][\n]"
The area of particular interest is the metadata part for which the structure is currently impossible to achieve as far as I can tell and the purpose of this feature request:
org.apache.hc.client5.http.wire : http-outgoing-0 >> "--nVCcViZpN810-A5mTBQ7AuGDfftezMFj_jPX[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Disposition: form-data; name="metadata"[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "Content-Type: application/json[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "{"value":"example"}[\r][\n]"
org.apache.hc.client5.http.wire : http-outgoing-0 >> "--nVCcViZpN810-A5mTBQ7AuGDfftezMFj_jPX--[\r][\n]"
I started some work on #1929 at the time but it fell dormant.
I have opened it for commentary around whether the approach would be of interest to be progressed.