Using "org.springframework:spring-web:5.3.23"
TLDR
the bug is in MappingJackson2HttpMessageConverter. For ByteArray method canRead() returns true, but method read() fails. Here is my github repository where you can reproduce this bug https://github.com/SpeedyGonzaless/SpringWebBug/tree/master
Research
If you add MappingJackson2HttpMessageConverter to list of HttpMessageConverters with your hands (for example with extending WebMvcConfigurationSupport and overriding configureMessageConverters)
@Configuration
class ConverterConfiguration(
private var builder: Jackson2ObjectMapperBuilder,
) : WebMvcConfigurationSupport() {
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>?>) {
converters.add(converter())
addDefaultHttpMessageConverters(converters)
}
@Bean
fun converter(): MappingJackson2HttpMessageConverter? {
val objectMapper = builder.build<ObjectMapper>()
return MappingJackson2HttpMessageConverter(objectMapper)
}
}
so the MappingJackson2HttpMessageConverter will be at the begging of list:
Then when you will try to make http request with ByteArray in body
@PostMapping
fun countries(
@RequestBody bytes: ByteArray
): String {
return "Success"
}
Your application will fail with exception
2022-09-22 09:25:21.914 WARN 48389 --- [nio-8083-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `[B` from Object value (token `JsonToken.START_OBJECT`); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `[B` from Object value (token `JsonToken.START_OBJECT`)<EOL> at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 1]]
The reason is in class AbstractMessageConverterMethodArgumentResolver, method readWithMessageConverters in this code:
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
As MappingJackson2HttpMessageConverter is first in the list we get it, check with the method canRead(), it returns true and then we get exception when call wethod read().
But if we remove this code
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>?>) {
converters.add(converter())
addDefaultHttpMessageConverters(converters)
}
then ByteArrayHttpMessageConverter would be first in list
and everything works fine
Result:
the bug is in MappingJackson2HttpMessageConverter. For ByteArray method canRead() returns true, but method read() fails.
Comment From: poutsma
The MappingJackson2HttpMessageConverter
is perfectly capable of reading (or writing) a byte array. For instance, this works fine:
byte[] bytes = "Foo Bar".getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
this.converter.write(bytes, MediaType.APPLICATION_JSON, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return bos;
}
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
});
System.out.println(bos.toString(StandardCharsets.UTF_8));
The above prints out: "Rm9vIEJhcg=="
, which is the BASE64 representation of "Foo Bar". Note that there is no wrapper JSON object, just BASE64.
If you want to handle a JSON response where the byte array is part of object, under the name bytes
like you posted in the screenshot, then you have to read the request into a request object, something like:
private static class MyBytes {
private byte[] bytes;
public byte[] getBytes() {
return this.bytes;
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
}
In short: the JSON you POST does not match the @RequestBody
annotated parameter.