Could not extract response no suitable HttpMessageConverter found for response type...

Could not extract response no suitable HttpMessageConverter found for response type...

SpringMVC, OpenFeign 사용 중 발생


🚨 문제


회사 서비스를 확장하던 중 발생한 이슈이다.

타 회사의 API에 우리 서비스를 연동해야 하는 상황이었다.

집에 오자마자 데모 프로젝트를 두개 만들어 해당 상황을 재현해봤는데, 재현이 아주 잘 된다.


2021-12-20 19:39:25.637 ERROR 3936 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.codec.DecodeException: Could not extract response: no suitable HttpMessageConverter found for response type [class io.github.shirohoo.client.api.ApiController$Response] and content type [text/html]] with root cause

org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class io.github.shirohoo.client.api.ApiController$Response] and content type [text/html]
	at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:126) 


🚧 원인


  • SpringMVC, OpenFeign 사용 중 발생
  • 원인: API 서버에서 응답하는 실 데이터는 application/json
    • 하지만 Content-Typetext/html 😒
    • 즉, 웹 표준을 지키지 않는 응답으로 인한 문제


🚧 재현


재현용 데모 프로젝트를 두개 만들었다.

응답 서버에서는 객체를 JSON으로 직렬화해 응답하면서 Content-Type=text/html 으로 헤더를 설정해볼 것이다.

  • Spring MVC, OpenFeign 프로젝트 = 요청 서버
  • Spring MVC 프로젝트 = 응답 서버


우선 표준을 제대로 지키지 않는 응답서버를 간단하게 하나 구현한다.


@Slf4j
@RestController
public class FakeResponseController {

    @PostMapping
    public ResponseEntity<Response> post(@RequestBody Request request) {
        log.info("request={}", request);
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.TEXT_HTML);
        Response httpBody = Response.builder()
                .responseCode(HttpStatus.OK.value())
                .responseMessage(HttpStatus.OK.getReasonPhrase())
                .body(request)
                .build();
        log.info("httpBody={}", httpBody);

        return ResponseEntity.status(HttpStatus.OK)
                .headers(httpHeaders)
                .body(httpBody);
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Request {

        private String phoneNumber;

        private long amount;

    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Response<T> {

        private int responseCode;

        private String responseMessage;

        private T body;

    }

}


그리고 OpenFeign을 이용해 응답서버에 HTTP 요청을 보내야 하므로 이를 구현한다.


@Slf4j
@RestController
@RequiredArgsConstructor
public class RequestController {

    private final ApiServerClient client;

    @GetMapping
    public Response get() {
        Request request = new Request("010-1234-5678", 50_000);
        Response response = client.post(request);
        log.info("response={}", response);
        return response;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Request {

        private String phoneNumber;

        private long amount;

    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Response {

        private int responseCode;

        private String responseMessage;

        private Request body;

    }

}


그리고 두 서버를 모두 기동하고 요청을 보냈다.


image


버그 상황이 재현된 모습


✅ 해결


stack trace만 보자면 Content-Type=text/html을 처리해주는 HttpMessageConverter가 없는 것 같다.

이를 직접 구현할까 고민하다, 앞으로 수십개 벤더의 API를 다 연동해야 하는데, 별별 상황이 더 나올 것 같아 gson 의존성을 추가해서 해결해보기로 결정했다.

gson을 프로젝트에 추가하고 Content-Type=text/html 이여도 처리하도록 확장해줄 것이다.


// file: 'build.gradle'
dependencies {
    ...
    implementation 'com.google.code.gson:gson:2.8.9' // 현 시점 최신 버전을 추가
    ...
}


@Component
public static class ExpandGsonHttpMessageConverter extends GsonHttpMessageConverter {

    public ExpandGsonHttpMessageConverter() {
        List<MediaType> types = Arrays.asList(
                new MediaType(MediaType.TEXT_HTML, DEFAULT_CHARSET),
                new MediaType(MediaType.TEXT_PLAIN, DEFAULT_CHARSET),
                new MediaType(MediaType.TEXT_XML, DEFAULT_CHARSET)
        );
        super.setSupportedMediaTypes(types);
    }

}


그리고 두 서버를 모두 재기동한 후 다시 요청을 보내보았다.

응답 서버의 로그


2021-12-20 20:02:55.853  INFO 30796 --- [nio-8081-exec-1] i.g.s.api.api.FakeResponseController     : request=FakeResponseController.Request(phoneNumber=010-1234-5678, amount=50000)
2021-12-20 20:02:55.854  INFO 30796 --- [nio-8081-exec-1] i.g.s.api.api.FakeResponseController     : httpBody=FakeResponseController.Response(responseCode=200, responseMessage=OK, body=FakeResponseController.Request(phoneNumber=010-1234-5678, amount=50000))


요청 서버의 로그


2021-12-20 20:02:55.870  INFO 9108 --- [nio-8080-exec-1] i.g.s.client.api.RequestController       : response=RequestController.Response(responseCode=200, responseMessage=OK, body=RequestController.Request(phoneNumber=010-1234-5678, amount=50000))


문제없이 아주 잘 된다.

내일 출근하면 적용해야겠다.



© 2022. All rights reserved.