sourcetip

Spring Webflux : Webclient : 본문 가져오기 오류

fileupload 2023. 10. 25. 23:42
반응형

Spring Webflux : Webclient : 본문 가져오기 오류

저는 스프링 웹 플럭스의 웹 클라이언트를 사용하고 있습니다.

WebClient.create()
            .post()
            .uri(url)
            .syncBody(body)
            .accept(MediaType.APPLICATION_JSON)
            .headers(headers)
            .exchange()
            .flatMap(clientResponse -> clientResponse.bodyToMono(tClass));

잘 작동하고 있습니다.제가 전화하는 웹 서비스의 오류(Ex 500 내부 오류)를 처리하고 싶습니다.일반적으로 저는 "stream"에 doOnError를 추가하고 상태 코드를 테스트하기 위한 Throwable입니다.

하지만 웹 서비스에서 제공하는 본체는 제가 사용하고 싶은 메시지를 제공하기 때문에 제 문제는 그것을 받고 싶습니다.

저는 무슨 일이 있어도 플랫맵을 하고 상태 코드를 스스로 테스트하여 바디를 역직렬화할지 여부를 확인하려고 합니다.

http 오류를 처리하고 예외를 처리하려면 클라이언트 응답에서 제공하는 메서드를 사용하는 것이 좋습니다.

WebClient.create()
         .post()
         .uri( url )
         .body( bodyObject == null ? null : BodyInserters.fromValue( bodyObject ) )
         .accept( MediaType.APPLICATION_JSON )
         .headers( headers )
         .exchange()
         .flatMap( clientResponse -> {
             //Error handling
             if ( clientResponse.statusCode().isError() ) { // or clientResponse.statusCode().value() >= 400
                 return clientResponse.createException().flatMap( Mono::error );
             }
             return clientResponse.bodyToMono( clazz )
         } )
         //You can do your checks: doOnError (..), onErrorReturn (..) ...
         ...

실제로 DefaultWebClient의 DefaultResponseSpec에서 오류를 처리하는 데 사용된 것과 같은 논리입니다.DefaultResponseSpec은 Exchange()가 아닌 retrieve()를 실행한 경우 발생할 ResponseSpec을 구현한 것입니다.

있지 않나요?onStatus()?

    public Mono<Void> cancel(SomeDTO requestDto) {
        return webClient.post().uri(SOME_URL)
                .body(fromObject(requestDto))
                .header("API_KEY", properties.getApiKey())
                .retrieve()
                .onStatus(HttpStatus::isError, response -> {
                    logTraceResponse(log, response);
                    return Mono.error(new IllegalStateException(
                            String.format("Failed! %s", requestDto.getCartId())
                    ));
                })
                .bodyToMono(Void.class)
                .timeout(timeout);
    }

그리고:

    public static void logTraceResponse(Logger log, ClientResponse response) {
        if (log.isTraceEnabled()) {
            log.trace("Response status: {}", response.statusCode());
            log.trace("Response headers: {}", response.headers().asHttpHeaders());
            response.bodyToMono(String.class)
                    .publishOn(Schedulers.elastic())
                    .subscribe(body -> log.trace("Response body: {}", body));
        }
    }

저는 이렇게 해서 오류 본체를 얻었습니다.

webClient
...
.retrieve()    
.onStatus(HttpStatus::isError, response -> response.bodyToMono(String.class) // error body as String or other class
                                                   .flatMap(error -> Mono.error(new RuntimeException(error)))) // throw a functional exception
.bodyToMono(MyResponseType.class)
.block();

당신도 이렇게 할 수 있습니다.

return webClient.getWebClient()
 .post()
 .uri("/api/Card")
 .body(BodyInserters.fromObject(cardObject))
 .exchange()
 .flatMap(clientResponse -> {
     if (clientResponse.statusCode().is5xxServerError()) {
        clientResponse.body((clientHttpResponse, context) -> {
           return clientHttpResponse.getBody();
        });
     return clientResponse.bodyToMono(String.class);
   }
   else
     return clientResponse.bodyToMono(String.class);
});

더 많은 예를 보려면 이 기사를 읽어 보십시오 링크, 오류 처리와 관련된 유사한 문제가 발생했을 때 도움이 된다는 것을 알게 되었습니다.

이 글을 쓰는 시점에서 5xx 오류는 더 이상 기본 Netty 계층에서 예외가 발생하지 않습니다.https://github.com/spring-projects/spring-framework/commit/b0ab84657b712aac59951420f4e9d696c3d84ba2 참조

저는 다음과 같은 일을 합니다.

Mono<ClientResponse> responseMono = requestSpec.exchange()
            .doOnNext(response -> {
                HttpStatus httpStatus = response.statusCode();
                if (httpStatus.is4xxClientError() || httpStatus.is5xxServerError()) {
                    throw new WebClientException(
                            "ClientResponse has erroneous status code: " + httpStatus.value() +
                                    " " + httpStatus.getReasonPhrase());
                }
            });

그 다음:

responseMono.subscribe(v -> { }, ex -> processError(ex));

"Reactor와 함께 예외를 던지는 올바른 방법"에 대한환상적인 SO 답변을 통해 이 답변을 종합할 수 있었습니다.사용합니다..onStatus,.bodyToMono,그리고..handle오류 응답 본문을 예외로 매핑합니다.

// create a chicken
webClient
    .post()
    .uri(urlService.getUrl(customer) + "/chickens")
    .contentType(MediaType.APPLICATION_JSON)
    .body(Mono.just(chickenCreateDto), ChickenCreateDto.class) // outbound request body
    .retrieve()
    .onStatus(HttpStatus::isError, clientResponse ->
        clientResponse.bodyToMono(ChickenCreateErrorDto.class)
            .handle((error, sink) -> 
                sink.error(new ChickenException(error))
            )
    )
    .bodyToMono(ChickenResponse.class)
    .subscribe(
            this::recordSuccessfulCreationOfChicken, // accepts ChickenResponse
            this::recordUnsuccessfulCreationOfChicken // accepts throwable (ChickenException)
    );

저는 방금 비슷한 상황에 직면했고 webClient가 4xx/5xx 응답을 받더라도 예외를 두지 않는다는 것을 알게 되었습니다.저의 경우 웹클라이언트를 이용하여 먼저 전화를 걸어 응답을 받고 2xx 응답을 회신하는 경우 응답에서 데이터를 추출하여 두 번째 전화를 걸 때 사용합니다.첫 번째 호출이 non-2xx 응답을 수신하는 경우 예외를 적용합니다.예외를 던지는 것이 아니기 때문에 첫 번째 호출이 실패했을 때도 두 번째 호출은 계속 수행됩니다.그래서 제가 한 것은.

return webClient.post().uri("URI")
    .header(HttpHeaders.CONTENT_TYPE, "XXXX")
    .header(HttpHeaders.ACCEPT, "XXXX")
    .header(HttpHeaders.AUTHORIZATION, "XXXX")
    .body(BodyInserters.fromObject(BODY))
    .exchange()
    .doOnSuccess(response -> {
        HttpStatus statusCode = response.statusCode();
        if (statusCode.is4xxClientError()) {
            throw new Exception(statusCode.toString());
        }
        if (statusCode.is5xxServerError()) {
            throw new Exception(statusCode.toString());
        }
    )
    .flatMap(response -> response.bodyToMono(ANY.class))
    .map(response -> response.getSomething())
    .flatMap(something -> callsSecondEndpoint(something));
}

Netty의 httpclient(HttpClientRequest)는 기본적으로 클라이언트 오류(4XX)가 아니라 서버 오류(response 5XX)에서 실패하도록 구성되어 있으므로 항상 예외가 발생했습니다.

우리가 수행한 작업은 AbstractClientHttpRequest 및 ClientHttpConnector를 확장하여 httpclient가 원하는 방식으로 동작하도록 구성하고 WebClient를 호출할 때 사용자 지정 ClientHttpConnector를 사용합니다.

 WebClient.builder().clientConnector(new CommonsReactorClientHttpConnector()).build();

WebClient의 retrieve() 메서드는 상태 코드가 4xx 또는 5xx인 응답이 수신될 때마다 WebClientResponseException을 발생시킵니다.

응답상태코드를확인하여예외처리가가능합니다.

   Mono<Object> result = webClient.get().uri(URL).exchange().log().flatMap(entity -> {
        HttpStatus statusCode = entity.statusCode();
        if (statusCode.is4xxClientError() || statusCode.is5xxServerError())
        {
            return Mono.error(new Exception(statusCode.toString()));
        }
        return Mono.just(entity);
    }).flatMap(clientResponse -> clientResponse.bodyToMono(JSONObject.class))

참조: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/

우연히 발견한 거라 코드를 올리는 게 낫겠다고 생각했어요

제가 한 일은 웹 클라이언트에서 나오는 요청과 응답 오류를 경력화하는 글로벌 핸들러를 만드는 것이었습니다.이것은 코틀린에 있지만, 당연히 자바로 쉽게 변환할 수 있습니다.이렇게 하면 기본 동작이 확장되므로 고객 처리 외에 모든 자동 구성을 사용할 수 있습니다.

이것은 사용자가 직접 사용하는 것이 아니라 웹 클라이언트 오류를 적절한 응답으로 변환하는 것입니다.응답 오류의 경우 코드와 응답 본문을 클라이언트에 전달하기만 하면 됩니다.현재 요청 오류의 경우 연결 문제만 처리합니다. 그 부분이 제가 신경 쓰는 부분이기 때문입니다. 그러나 보시다시피 쉽게 확장할 수 있습니다.

@Configuration
class WebExceptionConfig(private val serverProperties: ServerProperties) {

    @Bean
    @Order(-2)
    fun errorWebExceptionHandler(
        errorAttributes: ErrorAttributes,
        resourceProperties: ResourceProperties,
        webProperties: WebProperties,
        viewResolvers: ObjectProvider<ViewResolver>,
        serverCodecConfigurer: ServerCodecConfigurer,
        applicationContext: ApplicationContext
    ): ErrorWebExceptionHandler? {
        val exceptionHandler = CustomErrorWebExceptionHandler(
            errorAttributes,
            (if (resourceProperties.hasBeenCustomized()) resourceProperties else webProperties.resources) as WebProperties.Resources,
            serverProperties.error,
            applicationContext
        )
        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()))
        exceptionHandler.setMessageWriters(serverCodecConfigurer.writers)
        exceptionHandler.setMessageReaders(serverCodecConfigurer.readers)
        return exceptionHandler
    }
}

class CustomErrorWebExceptionHandler(
    errorAttributes: ErrorAttributes,
    resources: WebProperties.Resources,
    errorProperties: ErrorProperties,
    applicationContext: ApplicationContext
)  : DefaultErrorWebExceptionHandler(errorAttributes, resources, errorProperties, applicationContext) {

    override fun handle(exchange: ServerWebExchange, throwable: Throwable): Mono<Void> =
        when (throwable) {
            is WebClientRequestException -> handleWebClientRequestException(exchange, throwable)
            is WebClientResponseException -> handleWebClientResponseException(exchange, throwable)
            else -> super.handle(exchange, throwable)
        }

    private fun handleWebClientResponseException(exchange: ServerWebExchange, throwable: WebClientResponseException): Mono<Void> {
        exchange.response.headers.add("Content-Type", "application/json")
        exchange.response.statusCode = throwable.statusCode

        val responseBodyBuffer = exchange
            .response
            .bufferFactory()
            .wrap(throwable.responseBodyAsByteArray)

        return exchange.response.writeWith(Mono.just(responseBodyBuffer))
    }

    private fun handleWebClientRequestException(exchange: ServerWebExchange, throwable: WebClientRequestException): Mono<Void> {
        if (throwable.rootCause is ConnectException) {

            exchange.response.headers.add("Content-Type", "application/json")
            exchange.response.statusCode = HttpStatus.BAD_GATEWAY

            val responseBodyBuffer = exchange
                .response
                .bufferFactory()
                .wrap(ObjectMapper().writeValueAsBytes(customErrorWebException(exchange, HttpStatus.BAD_GATEWAY, throwable.message)))

            return exchange.response.writeWith(Mono.just(responseBodyBuffer))

        } else {
            return super.handle(exchange, throwable)
        }
    }

    private fun customErrorWebException(exchange: ServerWebExchange, status: HttpStatus, message: Any?) =
        CustomErrorWebException(
            Instant.now().toString(),
            exchange.request.path.value(),
            status.value(),
            status.reasonPhrase,
            message,
            exchange.request.id
        )
}

data class CustomErrorWebException(
    val timestamp: String,
    val path: String,
    val status: Int,
    val error: String,
    val message: Any?,
    val requestId: String,
)

사실, 당신은 그 시체를 쉽게 로그인 할 수 있습니다.onError호출:

            .doOnError {
                logger.warn { body(it) }
            }

그리고:

    private fun body(it: Throwable) =
        if (it is WebClientResponseException) {
            ", body: ${it.responseBodyAsString}"
        } else {
            ""
        }

500 내부 시스템 오류를 트리거한 WebClient 요청의 세부 정보를 원하는 경우 다음과 같이 DefaultErrorWebExceptionHandler를 재정의합니다.

Spring 기본값은 클라이언트에 오류가 발생했음을 알려주는 것이지만 WebClient 호출 본문을 제공하지 않으므로 디버깅 시 매우 귀중할 수 있습니다.

/**
 * Extends the DefaultErrorWebExceptionHandler to log the response body from a failed WebClient
 * response that results in a 500 Internal Server error.
 */
@Component
@Order(-2)
public class ExtendedErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {

  private static final Log logger = HttpLogging.forLogName(ExtendedErrorWebExceptionHandler.class);

  public FsErrorWebExceptionHandler(
      ErrorAttributes errorAttributes,
      Resources resources,
      ServerProperties serverProperties,
      ApplicationContext applicationContext,
      ServerCodecConfigurer serverCodecConfigurer) {
    super(errorAttributes, resources, serverProperties.getError(), applicationContext);
    super.setMessageWriters(serverCodecConfigurer.getWriters());
    super.setMessageReaders(serverCodecConfigurer.getReaders());
  }

  /**
   * Override the default error log behavior to provide details for WebClientResponseException. This
   * is so that administrators can better debug WebClient errors.
   *
   * @param request The request to the foundation service
   * @param response The response to the foundation service
   * @param throwable The error that occurred during processing the request
   */
  @Override
  protected void logError(ServerRequest request, ServerResponse response, Throwable throwable) {
    // When the throwable is a WebClientResponseException, also log the body
    if (HttpStatus.resolve(response.rawStatusCode()) != null
        && response.statusCode().equals(HttpStatus.INTERNAL_SERVER_ERROR)
        && throwable instanceof WebClientResponseException) {
      logger.error(
          LogMessage.of(
              () ->
                  String.format(
                      "%s 500 Server Error for %s\n%s",
                      request.exchange().getLogPrefix(),
                      formatRequest(request),
                      formatResponseError((WebClientResponseException) throwable))),
          throwable);
    } else {
      super.logError(request, response, throwable);
    }
  }

  private String formatRequest(ServerRequest request) {
    String rawQuery = request.uri().getRawQuery();
    String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
    return "HTTP " + request.methodName() + " \"" + request.path() + query + "\"";
  }

  private String formatResponseError(WebClientResponseException exception) {
    return String.format(
        "%-15s %s\n%-15s %s\n%-15s %d\n%-15s %s\n%-15s '%s'",
        "  Message:",
        exception.getMessage(),
        "  Status:",
        exception.getStatusText(),
        "  Status Code:",
        exception.getRawStatusCode(),
        "  Headers:",
        exception.getHeaders(),
        "  Body:",
        exception.getResponseBodyAsString());
  }
}

"Throwable e" 매개 변수를 WebClientResponseException에 캐스팅한 다음 getResponseBodyAsString():

    WebClient webClient = WebClient.create("https://httpstat.us/404");
    Mono<Object> monoObject = webClient.get().retrieve().bodyToMono(Object.class);
    monoObject.doOnError(e -> {
        if( e instanceof WebClientResponseException ){
            System.out.println(
                "ResponseBody = " + 
                    ((WebClientResponseException) e).getResponseBodyAsString() 
            );
        }
    }).subscribe();
    // Display : ResponseBody = 404 Not Found

언급URL : https://stackoverflow.com/questions/44593066/spring-webflux-webclient-get-body-on-error

반응형