Wrong content-type for multipart requests on JSON parts (text/plain instead of application/json)
I have the following request mapping defined which includes 2 parts:
- metadata: POJO (application/json)
- files: multipart
@RequestMapping(
method = RequestMethod.POST,
value = "/v1/files/bulk",
produces = { "application/json" },
consumes = "multipart/form-data"
)
ResponseEntity<Files> saveFiles(
@Parameter(name = "metadata", description = "Array of file attributes", required = true) @Valid @RequestPart(value = "metadata", required = true) List<@Valid Metadata> metadata,
@Parameter(name = "files", description = "Array of Files") @RequestPart(value = "files", required = false) List<MultipartFile> files
);
which produces the following body:
--195903cbf58
Content-Disposition: form-data; name="metadata"
Content-Type: text/plain; charset=UTF-8
[{"fileName":"somefileName","hash":"somehash"}]
--195903cbf58
Content-Disposition: form-data; name="files"; filename=""
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
--195903cbf58--
It shows that the content type of the JSON part is set to text/plain instead of application/json. This is due to the fact that feign.form.multipart.DelegateWriter#parameterWriter created in feign.form.MultipartFormContentProcessor#MultipartFormContentProcessor is set to feign.form.multipart.SingleParameterWriter which is using a hard-coded content-type of text/plain.
I have noticed that the RequestTemplate that is used to write the JSON data actually has the correct headers set ("Content-Type: application/json"). However, this is ignored as the hard-coded value is used.
I would expect that the content type is taken from the RequestTemplate.
The only workaround that I found is to do a String replace on the body like this:
public class SpringFormEncoderWithContentTypeCorrection extends SpringFormEncoder {
private static final String CONTENT_TYPE_STRING_TO_REPLACE = "Content-Type: text/plain; charset=";
private static final String NEW_CONTENT_TYPE_STRING = "Content-Type: application/json; charset=";
public SpringFormEncoderWithContentTypeCorrection(Encoder delegate) {
super(delegate);
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
super.encode(object, bodyType, template);
if (template.body() != null) {
// modify content type from 'text/plain' to 'application/json' for json parts
String modifiedContent = (new String(template.body())).replace(CONTENT_TYPE_STRING_TO_REPLACE, NEW_CONTENT_TYPE_STRING);
template.body(modifiedContent);
}
}
}
Configuration:
@Bean
@Primary
@Scope("prototype")
public Encoder feignFormEncoder(final ObjectFactory<HttpMessageConverters> messageConverters) {
return new SpringFormEncoderWithContentTypeCorrection(new SpringEncoder(messageConverters));
}
Versions used:
- openfeign: 13.5
- spring-cloud-openfeign-core: 4.2.0
- Spring Boot: 3.4.2
I found a less invasive workaround by create a custom JsonFormWriter and SpringFormEncoder.
/**
* Custom JSON form writer which writes everything as Json, except multipart files.
*/
public class CustomJsonFormWriter extends JsonFormWriter {
private final ObjectMapper objectMapper;
// These objects are just needed to check if the value is a multipart file
private final SpringManyMultipartFilesWriter springManyMultipartFilesWriter = new SpringManyMultipartFilesWriter();
private final SpringSingleMultipartFileWriter springSingleMultipartFileWriter = new SpringSingleMultipartFileWriter();
public CustomJsonFormWriter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean isApplicable(Object value) {
// Required because we can only add writers to the beginning of the list and our writer will be the first. See CustomSpringFormEncoder.
// The logic is needed because we want to use the objectMapper always, except for multipart files.
return !springSingleMultipartFileWriter.isApplicable(value) && !springManyMultipartFilesWriter.isApplicable(value);
}
@Override
protected String writeAsString(Object object) throws IOException {
return objectMapper.writeValueAsString(object);
}
}
/**
* Custom Feign form encoder which uses a custom JsonFormWriter to encode the request body as JSON. This is necessary to support JSON request bodies in multipart requests.
*/
public class CustomSpringFormEncoder extends SpringFormEncoder {
public CustomSpringFormEncoder(Encoder delegate, JsonFormWriter jsonFormWriter) {
super(delegate);
MultipartFormContentProcessor processors = (MultipartFormContentProcessor) getContentProcessor(MULTIPART);
processors.addFirstWriter(jsonFormWriter);
}
}
Spring Configuration:
@Bean
public CustomJsonFormWriter jsonFormWriter(ObjectMapper objectMapper) {
return new CustomJsonFormWriter(objectMapper);
}
@Bean
@Primary
@Scope("prototype")
public Encoder feignFormEncoder(final ObjectFactory<HttpMessageConverters> messageConverters, CustomJsonFormWriter customJsonFormWriter) {
return new CustomSpringFormEncoder(new SpringEncoder(messageConverters), customJsonFormWriter);
}
nice