Springboot

Content-Type 'application/octet-stream' is not supported 원인 및 해결 방법

땍땍 2025. 2. 5. 20:48
[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;
  }
}