HTTP请求出现HttpMediaTypeNotAcceptableException报错

现象

发起HTTP请求调用,控制台报错:

org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation

​ at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:322) ~[spring-webmvc-6.1.10.jar:6.1.10]


解决方式

定位报错位置

1
2
3
4
5
6
7
8
9
10
11
12
13
// org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)

if (body != null) {
Set<MediaType> producibleMediaTypes =
(Set<MediaType>) inputMessage.getServletRequest()
.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass()));
}

由报错位置可知,是body不为空引起。同时找到该方法正常return的位置如下。可见,正常返回与body是否为空无关,而是需要存在一个能写入指定类的转换器,**以及前提条件selectedMediaType != null**。

image-20240715160320214


排查报错

没有消息转换器可以写入指定类

通过debug可知,实际HTTP请求的场景:

image-20240715172942529

selectedMediaType 被设置为 "application/octet-stream" 时,这通常意味着请求或响应的内容类型被指定为二进制流,这种类型通常用于非文本数据,如图片、音频、视频等文件。也就是说,报错是因为没有消息转换器messageConvert可以写入指定类

项目中实际涉及的消息转换器:

  1. ByteArrayHttpMessageConverter 只能转换字节数组。
  2. StringHttpMessageConverter 只能转换字符串对象。
  3. ResourceHttpMessageConverter 只能转换Resource类及其子类。
  4. ResourceRegionHttpMessageConverter 不支持转换 非泛型类型且不是ResourceRegion类及其子类的对象。
  5. AllEncompassingFromHttpMessageConverter 只能转换MultiValueMap类及其子类。
  6. MappingJackson2HttpMessageConverter 支持 */*, application/json, application/*+json。

但是实际期望响应的内容类型为application/jsonJackson 的主要功能是处理结构化的文本格式,例如 JSON 和 XML,响应能被MappingJackson2HttpMessageConverter处理才对,需要继续溯源。而acceptableTypes来源于请求,请求没有限制,那么就只能是**producibleTypes不符合预期**。

1
2
3
4
5
6
7
8
9
10
11
12
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);


protected List<MediaType> getProducibleMediaTypes(
HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {

// 遍历消息转换器,查找能写入指定类的转换器
// ...

// 由于找不到能写入的messageConvert,因此producibleTypes只包含了{"*/*"}
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : new ArrayList<>(result));
}

响应无法被MappingJackson2HttpMessageConverter 处理

那么,为什么响应无法被MappingJackson2HttpMessageConverter 处理呢?

image-20240716112418095

梳理代码可知,MappingJackson2HttpMessageConverter继承自AbstractJackson2HttpMessageConverter由于AbstractJackson2HttpMessageConvertercanWrite()方法返回false,导致无法获得Jackson2Http消息转换器的支持媒体类型。canWrite()的本质是有对应的序列化器就能写入指定对象,由于实际项目中没有使用注解的序列化器,因此是通过findBeanOrAddOnSerializer()方法获得序列化器。

image-20240716113551448

获取Bean序列化器,首先就是查找属性。由于从POJOPropertiesCollector中获取到的_properties为空,导致BeanSerializerBuilder无法构建Bean序列化器,进而导致无法写入指定对象。

1
2
3
4
5
6
7
8
9
10
public JsonSerializer<?> build() {
// ...

if (_properties == null || _properties.isEmpty()) {
if (_anyGetter == null && _objectIdWriter == null) {
// NOTE! Caller may still call `createDummy()` later on
return null;
}
}
}

POJOPropertiesCollector_properties为空

需要找到设置_properties属性的位置,代码中仅POJOPropertiesCollector#collectAll方法进行了设置。而collectAll方法调用的位置很多,通过debug进行排查梳理,发现是**findSerializerByAnnotations()方法中有调用**。

image-20240716120202785

POJOPropertiesCollectorcollectAll方法,首先会将指定对象的属性添加到props中。重点来了!!removeUnwantedProperties()方法会调用POJOPropertyBuilder#anyVisible方法,将未提供get或set方法的private字段,认定为不想要的属性进行移除,进而设置_properties属性为null

查看指定对象的类,只有private属性和构造器…真相大白。提供get方法后,HTTP请求终于可以正常处理。

image-20240716120959156


参考

这个字段我明明传了呀,为什么收不到 - Spring 中首字母小写,第二个字母大写造成的参数问题 - 技术角落 - 博客园