[2025-02-03 22:48:44.371] [http-nio-8080-exec-8] [8706cf15-ff91-49be-98c7-236cb49d64c6] ERROR [.error.GlobalExceptionHandler.handleException:25 ] - Content-Type 'application/octet-stream' is not supported
[2025-02-03 22:48:44.372] [http-nio-8080-exec-8] [8706cf15-ff91-49be-98c7-236cb49d64c6] ERROR [.error.GlobalExceptionHandler.handleException:26 ] - Exception : org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported
프로젝트를 진행하던 중 2개 이상의 Content-Type을 필요로 하는 API는 스웨거에서 오류가 발생한다는 것을 확인하였다.
@PostMapping(value = "/add", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResult<add.Response> add(
@Valid @RequestPart add.Request req,
@RequestPart(value = "file", required = false) MultipartFile file
) {
add.Response response = service.add(req, file);
return ApiResult.success(response);
}
이러한 API는 테스트코드 혹은 PostMan과 같이 직접 Content-Type을 지정하여 요청을 보낼때는 정상적으로 작동하지만 스웨거에서만 오류가 발생한다는 것을 확인하였고 문제 해결 전, 정확한 원인을 확인하기 위해 따로 지정한적 없는 application/octet-stream 가 왜 Content-Type으로 지정되어있는 것인지 찾아보았다.
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
..생략..
MediaType contentType;
try {
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage(), this.getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
..생략..
}
문제는 AbstractMessageConverterMethodArgumentResolver 클래스에서 찾아볼 수 있었는데 해당 클래스의 readWithMessageConverters 메소드에서는 요청의 Content-Type가 존재하지 않을 때 요청의 Content-Type 값을 application/octet-stream으로 지정하는 것을 확인할 수가 있었다.
정리하면, 해당 오류는 스웨거에서 요청의 Content-Type을 지정해주지 못하여 발생하는 것으로 해결 방법은 크게 두 가지가 존재한다.
첫 번째는, Content-Type이 누락되지 않게 직접 입력 혹은 처리 하는 방법
두 번째는, application/octet-stream에 대한 처리를 하는 방법
이다.
이번에는 두 번째 방법을 활용하여 문제를 해결해보도록 하겠다.
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
..생략..
if (genericConverter != null) {
if (genericConverter.canRead(targetType, contextClass, contentType)) {
break;
}
} else if (targetClass != null && converter.canRead(targetClass, contentType)) {
break;
}
}
if (message.hasBody()) {
HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : converter.read(targetClass, msgToUse);
body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
} else {
body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
}
}
if (body == NO_VALUE && noContentType && !message.hasBody()) {
body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, NoContentTypeHttpMessageConverter.class);
}
} catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
} finally {
if (message != null && message.hasBody()) {
this.closeStreamIfNecessary(message.getBody());
}
}
if (body != NO_VALUE) {
LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
String formatted = LogFormatUtils.formatValue(body, !traceOn);
return "Read \\"" + contentType + "\\" to [" + formatted + "]";
});
return body;
} else if (httpMethod != null && SUPPORTED_METHODS.contains(httpMethod) && (!noContentType || message.hasBody())) {
throw new HttpMediaTypeNotSupportedException(contentType, this.getSupportedMediaTypes(targetClass != null ? targetClass : Object.class), httpMethod);
} else {
return null;
}
..생략..
}
다시 이전 readWithMessageConverters 메소드를 확인해보면, 오류를 발생하기 전 특정 Content-Type에 대한 Converter를 찾아보고, 이가 존재하지 않을 경우 HttpMediaTypeNotSupportedException 예외를 던지는 것을 확인할 수 있다.
이러한 예외를 발생하지 않기 위해 아래와 같이 application/octet-stream 에 대한 컨버터를 추가해 오류를 해결할 수가 있다.
@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}
'Springboot' 카테고리의 다른 글
Gateway 없이 서비스 분리 (0) | 2025.07.05 |
---|---|
Kafka 를 활용한 지연처리 (지연큐) (0) | 2025.06.21 |
스프링부트 통합 테스트 코드 작성법 및 예시( TestRestTemplate , MockMVC , WebTestClient 차이점 및 선택 방법) given-when-then (0) | 2025.01.28 |
Exception Handler ( 커스텀 익셉션 ) 사용 이유 및 작성 방법 (0) | 2024.11.25 |
fixtureMonkey, JakartaValidationPlugin 제약 조건 설정 (0) | 2024.10.06 |