I have a piece of code to send a request via WebClient:
public String getResponse(String requestBody){
...
final WebClient.RequestHeadersSpec<?> request =
client.post().body(BodyInserters.fromValue(requestBody));
final String resp =
req.retrieve().bodyToMono(String.class)
.doOnError(
WebClientResponseException.class,
err -> {
// do something
throw new InvalidRequestException(err.getResponseBodyAsString());
})
.block();
...
}
From the code, it looks like the InvalidRequestException will be thrown when the WebClientResponseException happens. However, the test throws the WebClientResponseException instead of the InvalidRequestException.
Here's the part of unit test I wrote to cover the .doOnError part. I tried the following:
...
when(webClientMock.post()).thenReturn(requestBodyUriMock);
when(requestBodyUriMock.body(any())).thenReturn(requestHeadersMock);
when(requestHeadersMock.retrieve()).thenReturn(responseMock);
when(responseMock.bodyToMono(String.class)).thenThrow(new WebClientResponseException(400, "Bad Request", null, null, null));
try {
String result = someServiceSpy.getResponse(requestBody);
} catch (InvalidRequestException e) {
assertEquals(expectedCode, e.getRawStatusCode());
}
The solution is I need to wrap the exception in Mono.error, and then the doOnError will be triggered.
when(responseMock.bodyToMono(String.class)).thenReturn(Mono.error(thrownException));
We have both asynchronous and synchronous calls implemented using retrofit and Either to map success/error. After adding the network interceptor asynchronous calls are returning bad responses(works fine on postman). I have tried adding a general error JSON response thinking Either is not able to catch the exceptions but still no luck. please suggest a fix or new approach
Interceptorclass -
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
try {
val response = chain.proceed(request)
val bodyString = response.body!!.string()
return response.newBuilder()
.body(ResponseBody.create(response.body?.contentType(), bodyString))
.build()
} catch (e: Exception) {
e.printStackTrace()
var msg = ""
when (e) {
is SocketTimeoutException -> {
msg = "Timeout - Please check your internet connection"
}
is UnknownHostException -> {
msg = "Unable to make a connection. Please check your internet"
}
is ConnectionShutdownException -> {
msg = "Connection shutdown. Please check your internet"
}
is IOException -> {
msg = "Server is unreachable, please try again later."
}
is IllegalStateException -> {
msg = "${e.message}"
}
else -> {
msg = "${e.message}"
}
}
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(999)
.message(msg)
.body(ResponseBody.create(null, "{${e}}")).build()
}
}`
client - val client1 = OkHttpClient.Builder() .addInterceptor(Interceptor) .build()
ServiceConfig.kt - Adding client as below
#Singleton
#Provides
#BaseUrl(BaseUrlType.SERVICES)
fun provideSupportRetrofit(
jsonConverters: Converter.Factory,
#HttpClient(ClientType.OAUTH) client: Call.Factory
): ServicesFactory {
return fakeServicesFactory ?: Retrofit.Builder()
.callFactory(client)
.baseUrl(baseUrlServices)
.addCallAdapterFactory(EitherCallAdapterFactory())
.addConverterFactory(EitherConverterFactory())
.addConverterFactory(FiberErrorConverterFactory())
.addConverterFactory(jsonConverters)
.addConverterFactory(primitiveTypeConverters)
.client(client1)
.build()
.asFactory
}
EitherCovertor.kt -
`override fun enqueue(callback: Callback<Either<*, *>>) {
call.enqueue(object : Callback<Either<*, *>> {
override fun onResponse(call: Call<Either<*, *>>, response: Response<Either<*, *>>) {
callback.onResponse(this#EitherCall, response.asEither)
}
}
override fun onFailure(call: Call<Either<*, *>>, t: Throwable) {
when (t) {
is Error -> {
Timber.e("Failure Error from API")
callback.onFailure(call, t)
}
else -> callback.onResponse(this#EitherCall, t.asEither)
}
}
})
}`
To get the error text in the header section
headers["Accept"] = "application/json"
and process the error in json form, this way worked for me
Response.ErrorListener { error: VolleyError? ->
if (error is TimeoutError || error is NoConnectionError) {
Constants.checkInternet(context)
} else if (error is ServerError) {
val responseBody = String(error.networkResponse.data, Charsets.UTF_8)
val errorMsg: String =
JSONObject(responseBody).getJSONObject(TAG_META).getJSONObject(
TAG_STATUS)
.getString(TAG_MESSAGE)
Toast.makeText(context, errorMsg + "", Toast.LENGTH_SHORT)
.show()
Log.e("responseBody", responseBody)
Log.e("errorMsg", errorMsg)
}
Loading.hide(loading)
}
I have a Spring Boot 2.3.1 project, in which I use WebClient to call a remote service.
The remote service is not very reliable and tends to return 500 errors, with and without response bodies. My goal is throw a custom exception that contains the response body (or a default message) so that I can log it, and here's my code :
webClient.get()
.uri(targetServiceUri)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(error ->
Mono.error(new MyCustomServiceException(error))
)
)
.toEntity(String.class)
.block();
I have 2 tests using wiremock, the first one works :
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,"{'Error':'invalid request'}");
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("{'Error':'invalid request'}");
}
private void setStubForInValidCheckCall(HttpStatus httpStatus, String body) {
var response= aResponse().withStatus(httpStatus.value());
if(body!=null){
response=response.withBody(body);
}
stubFor(
get(urlPathMatching("/targetCompliance"))
.willReturn(response));
}
However, the second test in which the response is 500 but there's no body (or if it's an empty string), fails with "java.lang.IllegalStateException: Only one connection receive subscriber allowed.
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,null);
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("service returned status 500");
}
I am struggling to understand why this happens, and how to fix it..
is it "normal" ? or am I missing something obvious (is it a problem with my test ?) ?
I have found a workaround, but it doesn't feel "webFlux-y" at all, and I still don't understand why the Only one connection receive subscriber allowed was happening :
try {
ResponseEntity<String> responseEntity =
webClient.get()
.uri(targetServiceUri)
.retrieve()
.toEntity(String.class)
.block();
}
catch (WebClientException e) {
if(e instanceof InternalServerError){
var internalServerError=(InternalServerError) e;
if(internalServerError.getStatusCode().is5xxServerError()){
var respBody=internalServerError.getResponseBodyAsString();
if(StringUtils.isEmpty(respBody)){
respBody=MY_STANDARD_MESSAGE +internalServerError.getRawStatusCode() ;
}
throw new MyCustomServiceException(respBody);
}
}
}
I am passing null right now, which causes a crash!
See: val response: Response<ReviewResponse> = Response.error(-1, null)
Code:
suspend fun getReviewData() = getResult {
try {
apiService.getReviewData(getCustomerId())
} catch (e: Exception) {
val response: Response<ReviewResponse> = Response.error(-1, null)
response
}
}
As you can see null is not accepting internally, and I must need pass this: ResponseBody body
What about this:
Response.error(404, ResponseBody.create(null, "Not found response body"))
You can create a result data class like this
data class ApiResult<out T>(
val status: Status,
val data: T?,
val error: Throwable?,
val message: String?
) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T?): ApiResult<T> {
return ApiResult(Status.SUCCESS, data, null, null)
}
fun <T> error(message: String, error: Throwable?): ApiResult<T> {
return ApiResult(Status.ERROR, null, error, message)
}
fun <T> loading(data: T? = null): ApiResult<T> {
return ApiResult(Status.LOADING, data, null, null)
}
}
override fun toString(): String {
return "Result(status=$status, data=$data, error=$error, message=$message)"
}
}
and then create your custom base response like this
data class CommonResponse<T>(
#SerializedName("error") val error: Boolean,
#SerializedName("status") val status: Int,
#SerializedName("message") val message: String?,
#SerializedName("response") val response: T?
)
and assign them like this in retrofit
suspend fun <T> getResponse(
request: suspend () -> Response<T>
): ApiResult<T> {
return try {
val result = request.invoke()
if (result.isSuccessful) {
return ApiResult.success(result.body())
} else {
ApiResult.error("Error", null)
}
} catch (e: Throwable) {
ApiResult.error("Unkown Error", e)
}
}
and use them like this in call
interface CheckWhereApi {
//Check Where API
#GET("url")
suspend fun checkWhere(): Response<CommonResponse<MyModel>>
}
Depend on your requirement
addBody() function you are not passing any param here. need to pass param.
look like api construction in your code missing .
please follow link to know more-
https://www.chillcoding.com/android-retrofit-send-http/
++ update
depend on your comment i think you not get direct answer , i am not give direct answer, its depend on exact architecture you following.
little bit more info
java.lang.IllegalArgumentException: code < 400: -1
its define architecture.
if (code < 400) throw new IllegalArgumentException("code < 400: " + code);
here i suggest you how you going to return result its quite complicated , you try with some custom class with error handle and success handle.
data class ResponseByApi(val success: Any, val code: Int, val
error : Any)
create response model class and set value as per network response
like success set success body and code else if fail set error body and code -> return as per response.
I see that the code looks:
public static <T> Response<T> error(int code, ResponseBody body) {
Objects.requireNonNull(body, "body == null");
if (code < 400) throw new IllegalArgumentException("code < 400: " + code);
return ... // build error response
}
And you call it:
val response: Response = Response.error(-1, null)
Thus, it will fail by NullPointerException.
Even if you comment on this line, it will fail by IllegalArgumentException because the code is less than 400.
However, you need to return Response<ReviewResponse> type.
You could use ResponseEntity for this:
new ResponseEntity<>(HttpStatus.OK);
It will be exactly creating dummy ResponseBody object which required by OkHttp. But you need to use ResponseEntity instead of Response.
Or you could throw exception, like:
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "entity not found"
);
From org.springframework.web.server.ResponseStatusException
I am using the webclient from spring webflux, like this :
WebClient.create()
.post()
.uri(url)
.syncBody(body)
.accept(MediaType.APPLICATION_JSON)
.headers(headers)
.exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(tClass));
It is working well.
I now want to handle the error from the webservice I am calling (Ex 500 internal error). Normally i would add an doOnError on the "stream" and isu the Throwable to test the status code,
But my issue is that I want to get the body provided by the webservice because it is providing me a message that i would like to use.
I am looking to do the flatMap whatever happen and test myself the status code to deserialize or not the body.
I prefer to use the methods provided by the ClientResponse to handle http errors and throw exceptions:
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 (..) ...
...
In fact, it's the same logic used in the DefaultResponseSpec of DefaultWebClient to handle errors. The DefaultResponseSpec is an implementation of ResponseSpec that we would have if we made a retrieve() instead of exchange().
Don't we have 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);
}
And:
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));
}
}
I got the error body by doing like this:
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();
You could also do this
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);
});
Read this article for more examples link, I found it to be helpful when I experienced a similar problem with error handling
I do something like this:
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());
}
});
and then:
responseMono.subscribe(v -> { }, ex -> processError(ex));
Note that as of writing this, 5xx errors no longer result in an exception from the underlying Netty layer. See https://github.com/spring-projects/spring-framework/commit/b0ab84657b712aac59951420f4e9d696c3d84ba2
I had just faced the similar situation and I found out webClient does not throw any exception even it is getting 4xx/5xx responses. In my case, I use webclient to first make a call to get the response and if it is returning 2xx response then I extract the data from the response and use it for making the second call. If the first call is getting non-2xx response then throw an exception. Because it is not throwing exception so when the first call failed and the second is still be carried on. So what I did is
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));
}
Using what I learned this fantastic SO answer regarding the "Correct way of throwing exceptions with Reactor", I was able to put this answer together. It uses .onStatus, .bodyToMono, and .handle to map the error response body to an exception.
// 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)
);
We have finally understood what is happening :
By default the Netty's httpclient (HttpClientRequest) is configured to fail on server error (response 5XX) and not on client error (4XX), this is why it was always emitting an exception.
What we have done is extend AbstractClientHttpRequest and ClientHttpConnector to configure the httpclient behave the way the want and when we are invoking the WebClient we use our custom ClientHttpConnector :
WebClient.builder().clientConnector(new CommonsReactorClientHttpConnector()).build();
The retrieve() method in WebClient throws a WebClientResponseException
whenever a response with status code 4xx or 5xx is received.
You can handle the exception by checking the response status code.
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))
Reference: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/
I stumbled across this so figured I might as well post my code.
What I did was create a global handler that takes career of request and response errors coming out of the web client. This is in Kotlin but can be easily converted to Java, of course. This extends the default behavior so you can be sure to get all of the automatic configuration on top of your customer handling.
As you can see this doesn't really do anything custom, it just translates the web client errors into relevant responses. For response errors the code and response body are simply passed through to the client. For request errors currently it just handles connection troubles because that's all I care about (at the moment), but as you can see it can be easily extended.
#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,
)
Actually, you can log the body easily in the onError call:
.doOnError {
logger.warn { body(it) }
}
and:
private fun body(it: Throwable) =
if (it is WebClientResponseException) {
", body: ${it.responseBodyAsString}"
} else {
""
}
For those that wish to the details of a WebClient request that triggered a 500 Internal System error, override the DefaultErrorWebExceptionHandler like as follows.
The Spring default is to tell you the client had an error, but it does not provide the body of the WebClient call, which can be invaluable in debugging.
/**
* 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());
}
}
You have to cast the "Throwable e" parameter to WebClientResponseException, then you can call 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