I have the following set of classes:
data class ApiResponse<T>(val httpCode: String? = null, val data: T? = null, val status: String? = null, val error: String? = null)
data class Customer(val name: String? = null)
data class User(val userId: String = "", val name: String? = null, val description: String? = null)
Basically, I will use the ApiResponse class to store an instance of Customer or User or sometimes a Map<String, Any?> in the data field generically. With these objects, I wrote this method:
private fun handleError(errorJson: String?): ApiResponse<Customer> {
var response: ApiResponse<Customer>
try {
response = objectMapper.readValue(errorJson, object: TypeReference<ApiResponse<Customer>>(){})
} catch (ex: Exception) {
// unable to parse
response = ApiResponse(
status = "500",
error = "Unknown error"
)
}
return response
}
Basically, my question is how do I make this method more generic so the same code can be used for both Customer,User, Map<String, Any?> objects? I would know when I am invoking this method the expected return type.
You're looking for reified type parameters:
private inline fun <reified T> handleError(errorJson: String?): ApiResponse<T> {
var response: ApiResponse<Customer>
try {
response = objectMapper.readValue(errorJson, object: TypeReference<ApiResponse<T>>(){})
} catch (ex: Exception) {
// unable to parse
response = ApiResponse(
status = "500",
error = "Unknown error"
)
}
return response
}
I need to create a retrofit call adapter which can handle such network calls:
#GET("user")
suspend fun getUser(): MyResponseWrapper<User>
I want it to work with Kotlin Coroutines without using Deferred. I have already have a successful implementation using Deferred, which can handle methods such as:
#GET("user")
fun getUser(): Deferred<MyResponseWrapper<User>>
But I want the ability make the function a suspending function and remove the Deferred wrapper.
With suspending functions, Retrofit works as if there is a Call wrapper around the return type, so suspend fun getUser(): User is treated as fun getUser(): Call<User>
My Implementation
I have tried to create a call adapter which tries to handle this. Here is my implementation so far:
Factory
class MyWrapperAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
val rawType = getRawType(returnType)
if (rawType == Call::class.java) {
returnType as? ParameterizedType
?: throw IllegalStateException("$returnType must be parameterized")
val containerType = getParameterUpperBound(0, returnType)
if (getRawType(containerType) != MyWrapper::class.java) {
return null
}
containerType as? ParameterizedType
?: throw IllegalStateException("MyWrapper must be parameterized")
val successBodyType = getParameterUpperBound(0, containerType)
val errorBodyType = getParameterUpperBound(1, containerType)
val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>(
null,
errorBodyType,
annotations
)
return MyWrapperAdapter<Any, Any>(successBodyType, errorBodyConverter)
}
return null
}
Adapter
class MyWrapperAdapter<T : Any>(
private val successBodyType: Type
) : CallAdapter<T, MyWrapper<T>> {
override fun adapt(call: Call<T>): MyWrapper<T> {
return try {
call.execute().toMyWrapper<T>()
} catch (e: IOException) {
e.toNetworkErrorWrapper()
}
}
override fun responseType(): Type = successBodyType
}
runBlocking {
val user: MyWrapper<User> = service.getUser()
}
Everything works as expected using this implementation, but just before the result of the network call is delivered to the user variable, I get the following error:
java.lang.ClassCastException: com.myproject.MyWrapper cannot be cast to retrofit2.Call
at retrofit2.HttpServiceMethod$SuspendForBody.adapt(HttpServiceMethod.java:185)
at retrofit2.HttpServiceMethod.invoke(HttpServiceMethod.java:132)
at retrofit2.Retrofit$1.invoke(Retrofit.java:149)
at com.sun.proxy.$Proxy6.getText(Unknown Source)
...
From Retrofit's source, here is the piece of code at HttpServiceMethod.java:185:
#Override protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call); // ERROR OCCURS HERE
//noinspection unchecked Checked by reflection inside RequestFactory.
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
}
I'm not sure how to handle this error. Is there a way to fix?
Here is a working example of an adapter, which automatically wraps a response to the Result wrapper. A GitHub sample is also available.
// build.gradle
...
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation 'com.google.code.gson:gson:2.8.5'
}
// test.kt
...
sealed class Result<out T> {
data class Success<T>(val data: T?) : Result<T>()
data class Failure(val statusCode: Int?) : Result<Nothing>()
object NetworkError : Result<Nothing>()
}
data class Bar(
#SerializedName("foo")
val foo: String
)
interface Service {
#GET("bar")
suspend fun getBar(): Result<Bar>
#GET("bars")
suspend fun getBars(): Result<List<Bar>>
}
abstract class CallDelegate<TIn, TOut>(
protected val proxy: Call<TIn>
) : Call<TOut> {
override fun execute(): Response<TOut> = throw NotImplementedError()
override final fun enqueue(callback: Callback<TOut>) = enqueueImpl(callback)
override final fun clone(): Call<TOut> = cloneImpl()
override fun cancel() = proxy.cancel()
override fun request(): Request = proxy.request()
override fun isExecuted() = proxy.isExecuted
override fun isCanceled() = proxy.isCanceled
abstract fun enqueueImpl(callback: Callback<TOut>)
abstract fun cloneImpl(): Call<TOut>
}
class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Result<T>>(proxy) {
override fun enqueueImpl(callback: Callback<Result<T>>) = proxy.enqueue(object: Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val code = response.code()
val result = if (code in 200 until 300) {
val body = response.body()
Result.Success(body)
} else {
Result.Failure(code)
}
callback.onResponse(this#ResultCall, Response.success(result))
}
override fun onFailure(call: Call<T>, t: Throwable) {
val result = if (t is IOException) {
Result.NetworkError
} else {
Result.Failure(null)
}
callback.onResponse(this#ResultCall, Response.success(result))
}
})
override fun cloneImpl() = ResultCall(proxy.clone())
}
class ResultAdapter(
private val type: Type
): CallAdapter<Type, Call<Result<Type>>> {
override fun responseType() = type
override fun adapt(call: Call<Type>): Call<Result<Type>> = ResultCall(call)
}
class MyCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
) = when (getRawType(returnType)) {
Call::class.java -> {
val callType = getParameterUpperBound(0, returnType as ParameterizedType)
when (getRawType(callType)) {
Result::class.java -> {
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
ResultAdapter(resultType)
}
else -> null
}
}
else -> null
}
}
/**
* A Mock interceptor that returns a test data
*/
class MockInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val response = when (chain.request().url().encodedPath()) {
"/bar" -> """{"foo":"baz"}"""
"/bars" -> """[{"foo":"baz1"},{"foo":"baz2"}]"""
else -> throw Error("unknown request")
}
val mediaType = MediaType.parse("application/json")
val responseBody = ResponseBody.create(mediaType, response)
return okhttp3.Response.Builder()
.protocol(Protocol.HTTP_1_0)
.request(chain.request())
.code(200)
.message("")
.body(responseBody)
.build()
}
}
suspend fun test() {
val mockInterceptor = MockInterceptor()
val mockClient = OkHttpClient.Builder()
.addInterceptor(mockInterceptor)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://mock.com/")
.client(mockClient)
.addCallAdapterFactory(MyCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(Service::class.java)
val bar = service.getBar()
val bars = service.getBars()
...
}
...
When you use Retrofit 2.6.0 with coroutines you don't need a wrapper anymore. It should look like below:
#GET("user")
suspend fun getUser(): User
You don't need MyResponseWrapper anymore, and when you call it, it should look like
runBlocking {
val user: User = service.getUser()
}
To get the retrofit Response you can do the following:
#GET("user")
suspend fun getUser(): Response<User>
You also don't need the MyWrapperAdapterFactory or the MyWrapperAdapter.
Hope this answered your question!
Edit
CommonsWare# has also mentioned this in the comments above
Edit
Handling error could be as follow:
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
return if(response.isSuccessful) {
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
Where you just need to call create with the response as ApiResponse.create(response) and it should return correct type. A more advanced scenario could be added here as well, by parsing the error if it is not just a plain string.
This question came up in the pull request where suspend was introduced to Retrofit.
matejdro: From what I see, this MR completely bypasses call adapters when using suspend functions. I'm currently using custom call adapters for centralising parsing of error body (and then throwing appropriate exceptions), smilarly to the official retrofit2 sample. Any chance we get alternative to this, some kind of adapter that is injected between here?
It turns out this is not supported (yet?).
Source: https://github.com/square/retrofit/pull/2886#issuecomment-438936312
For error handling I went for something like this to invoke api calls:
suspend fun <T : Any> safeApiCall(call: suspend () -> Response<T>): MyWrapper<T> {
return try {
val response = call.invoke()
when (response.code()) {
// return MyWrapper based on response code
// MyWrapper is sealed class with subclasses Success and Failure
}
} catch (error: Throwable) {
Failure(error)
}
}
I have a stream:
val symbols: Single<List<Symbol>>
Now I want to transform the stream into a UI State with map():
private fun cool(): Single<SymbolContract.State> =
symbols.map { SymbolContract.State.Symbols(it) }
What I want to do is catch an error on the upstream symbols single, so that I can catch any errors and then return SymbolContract.State.GeneralError().
I want something like a onErrorMap() or something. Unfortunately, putting onErrorResumeItem on symbols doesn't work because it needs to return a List<Symbol>.
I can think of a few ugly ways to do this, but what's the cleanest?
I suggest you to use global handling error. I give you a sample so you can get the idea. (It is kotlin) and you can catch as many as exception you would like, some of them are my custom exceptions. Just bear in mind, this sample is about Reactive Webflux but you get the idea. It would be similar in others
#Configuration
class ExceptionTranslator {
#Bean
#Order(-1)
fun handle(objectMapper: ObjectMapper): ErrorWebExceptionHandler {
return ErrorWebExceptionHandler { exchange, ex ->
if (exchange.response.isCommitted) {
return#ErrorWebExceptionHandler Mono.error(ex)
}
val response = exchange.response
response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
response.headers.contentType = MediaType.APPLICATION_PROBLEM_JSON_UTF8
val url: String
var message = ex.message
var params = mutableMapOf<String, Any>()
when (ex) {
is ParametricException -> {
url = ex.url
params = ex.params
}
is BaseException -> {
url = ex.url
}
is BadCredentialsException -> {
url = INVALID_CREDENTIAL_TYPE
message = ex.message ?: "Wrong Credentials"
}
is ConcurrencyFailureException -> {
url = INTERNAL_TYPE
message = ERR_CONCURRENCY_FAILURE
}
is MethodArgumentNotValidException -> {
val result = ex.bindingResult
val fieldErrors = result.fieldErrors.map {
FieldErrorVM(it.objectName, it.field, it.code ?: "Unknown")
}
url = CONSTRAINT_VIOLATION_TYPE
message = ERR_VALIDATION
params = Collections.singletonMap("errors", fieldErrors)
}
else -> url = INTERNAL_TYPE
}
if (ex is BaseException) {
response.statusCode = HttpStatus.valueOf(ex.status.code())
}
val bytes = objectMapper.writeValueAsBytes(ProblemVM(url, message ?: "Internal Error", params))
val buffer = response.bufferFactory().wrap(bytes)
response.writeWith(Mono.just(buffer))
}
}
}
Found a clean answer:
private fun symbols(): Single<SymbolContract.State> =
symbols.map<SymbolContract.State> { SymbolContract.State.Symbols(it) }
.onErrorReturnItem(SymbolContract.State.Error(SymbolContract.State.Error.Type.Network))
The onErrorReturnItem has to come after the map, and the map needs explicit type parameters.
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
When I try to build my Kotlin project I get the following error in Idea:
Error:Kotlin: [Internal Error] org.jetbrains.kotlin.util.KotlinFrontEndException: Exception while analyzing expression at (60,19) in E:/altruix-is/src/main/kotlin/com/mycompany/myproduct/capsulecrm/CapsuleCrmSubsystem.kt:
client.execute(req)
[...]
Caused by: java.lang.UnsupportedOperationException: doSubstitute with no original should not be called for synthetic extension
at org.jetbrains.kotlin.synthetic.SamAdapterFunctionsScope$MyFunctionDescriptor.doSubstitute(SamAdapterFunctionsScope.kt:165)
at org.jetbrains.kotlin.descriptors.impl.FunctionDescriptorImpl$CopyConfiguration.build(FunctionDescriptorImpl.java:553)
at org.jetbrains.kotlin.load.java.ErasedOverridabilityCondition.isOverridable(ErasedOverridabilityCondition.kt:47)
The error seems to occur in calls
res = client.execute(req)
where client is Apache HttpClient.
The source file, where this occurs can be found here.
I submitted the bug report to JetBrains, but I need to work further on the project and therefore a work around. Note that until yesterday everything worked fine. Yesterday I upgraded the Kotlin plugin to the most recent version, maybe that's the problem.
How can I avoid the error above?
Update 1 (03.03.2017 14:46 MSK):
This doesn't work:
open fun addNote(note: String, compId: Long): ValidationResult {
val client = httpClient
if (client == null) {
return ValidationResult(false, "Internal error")
}
var res: CloseableHttpResponse? = null
var req: HttpUriRequest?
try {
req = composeAddNoteRequest(note, compId)
res = client.execute(req)
if (res.statusLine.statusCode != 201) {
logger.error("addNote(note='$note', compId=$compId): Wrong status code ${res.statusLine.statusCode}")
return ValidationResult(false, "Wrong status code (CRM interaction)")
}
return ValidationResult(true, "")
}
catch (throwable: Throwable) {
logger.error("addNote(note='$note', compId=$compId)", throwable)
return ValidationResult(false, "Database error")
} finally {
close(res)
}
return ValidationResult(false, "Internal logic error")
}
This works (difference is in the second line from the top):
open fun addNote(note: String, compId: Long): ValidationResult {
val client = httpClient as CloseableHttpClient // Change
if (client == null) {
return ValidationResult(false, "Internal error")
}
var res: CloseableHttpResponse? = null
var req: HttpUriRequest?
try {
req = composeAddNoteRequest(note, compId)
res = client.execute(req)
if (res.statusLine.statusCode != 201) {
logger.error("addNote(note='$note', compId=$compId): Wrong status code ${res.statusLine.statusCode}")
return ValidationResult(false, "Wrong status code (CRM interaction)")
}
return ValidationResult(true, "")
}
catch (throwable: Throwable) {
logger.error("addNote(note='$note', compId=$compId)", throwable)
return ValidationResult(false, "Database error")
} finally {
close(res)
}
return ValidationResult(false, "Internal logic error")
}
In your example above, client.execute(req) returns HttpResponse, which is not a sub-type of CloseableHttpResponse. So, type mismatch error is correct. Either you should use CloseableHttpClient here, or cast client.execute(req) to CloseableHttpResponse.
I couldn't reproduce KotlinFrontEndException from your example. From the stack trace provided I can deduce that something wrong happened with "SAM adapters" - that is, when you use a Kotlin lambda in a Java method call that accepts single abstract method (SAM) interface.
Please, file a bug here if the problem still occurs: http://kotl.in/issue