From 141eb36d3cc34cb1a3851403276d9d55f2e444e1 Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 7 Jun 2023 10:22:01 +0200 Subject: [PATCH] Provide better stacktrace when throwing web exceptions in Rest Client Before these changes, when the server returns a 400 HTTP status, the following exception was thrown: ``` 2023-06-06 08:34:49,262 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /test/callMeFromSwagger failed, error id: 1ef6343d-3100-4dc1-8ac0-cfe8b829de5a-1: org.jboss.resteasy.reactive.ClientWebApplicationException: Received: 'Bad Request, status code 400' when invoking: Rest Client method: 'org.example.SelfClient#callMeWithRestClient' at org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.unwrapException(RestClientRequestContext.java:185) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.handleException(AbstractResteasyReactiveContext.java:322) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:173) at org.jboss.resteasy.reactive.client.impl.RestClientRequestContext$1.lambda$execute$0(RestClientRequestContext.java:295) at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:264) at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:246) at io.vertx.core.impl.EventLoopContext.lambda$runOnContext$0(EventLoopContext.java:43) at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174) at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167) at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:566) at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Thread.java:833) Caused by: jakarta.ws.rs.WebApplicationException: Bad Request, status code 400 at io.quarkus.rest.client.reactive.runtime.DefaultMicroprofileRestClientExceptionMapper.toThrowable(DefaultMicroprofileRestClientExceptionMapper.java:18) at io.quarkus.rest.client.reactive.runtime.MicroProfileRestClientResponseFilter.filter(MicroProfileRestClientResponseFilter.java:52) at org.jboss.resteasy.reactive.client.handlers.ClientResponseFilterRestHandler.handle(ClientResponseFilterRestHandler.java:21) at org.jboss.resteasy.reactive.client.handlers.ClientResponseFilterRestHandler.handle(ClientResponseFilterRestHandler.java:10) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.invokeHandler(AbstractResteasyReactiveContext.java:229) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:145) ... 12 more ``` The `DefaultMicroprofileRestClientExceptionMapper` is the REST Client reactive exception mapper by default, so it does not add the correct location of the REST Client caller. After these changes, the same exception would look like: ``` 2023-06-07 10:00:23,585 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /test/callMeFromSwagger failed, error id: 9863247d-43d8-40d9-8afa-52cdb96afe30-1: org.jboss.resteasy.reactive.ClientWebApplicationException: Received: 'Bad Request, status code 400' when invoking: Rest Client method: 'org.example.SelfClient#callMeWithRestClient' at org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.unwrapException(RestClientRequestContext.java:185) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.handleException(AbstractResteasyReactiveContext.java:322) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:173) at org.jboss.resteasy.reactive.client.impl.RestClientRequestContext$1.lambda$execute$0(RestClientRequestContext.java:302) at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:264) at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:246) at io.vertx.core.impl.EventLoopContext.lambda$runOnContext$0(EventLoopContext.java:43) at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174) at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167) at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:566) at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Thread.java:833) Caused by: jakarta.ws.rs.WebApplicationException: Bad Request, status code 400 at org.example.TestResource.callMeFromSwagger(TestResource.java:21) at org.example.TestResource$quarkusrestinvoker$callMeFromSwagger_f32a697cbab7bef8d94d98e774647d90970f8771.invoke(Unknown Source) at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29) at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:145) at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:576) ``` This is related to https://github.com/quarkusio/quarkus/discussions/33837 --- ...MicroprofileRestClientExceptionMapper.java | 15 +++++- ...lientCaptureCurrentContextRestHandler.java | 49 +++++++++++++++++++ .../client/impl/ClientResponseImpl.java | 4 ++ .../reactive/client/impl/HandlerChain.java | 7 ++- .../client/impl/RestClientRequestContext.java | 12 +++-- 5 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientCaptureCurrentContextRestHandler.java diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/DefaultMicroprofileRestClientExceptionMapper.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/DefaultMicroprofileRestClientExceptionMapper.java index d4653cc190271..02f1037cb9834 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/DefaultMicroprofileRestClientExceptionMapper.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/DefaultMicroprofileRestClientExceptionMapper.java @@ -5,18 +5,29 @@ import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.jboss.resteasy.reactive.client.impl.ClientResponseImpl; public class DefaultMicroprofileRestClientExceptionMapper implements ResponseExceptionMapper { public Throwable toThrowable(Response response) { try { response.bufferEntity(); - } catch (Exception var3) { + } catch (Exception ignored) { } - return new WebApplicationException( + WebApplicationException exception = new WebApplicationException( String.format("%s, status code %d", response.getStatusInfo().getReasonPhrase(), response.getStatus()), response); + + if (response instanceof ClientResponseImpl) { + ClientResponseImpl clientResponse = (ClientResponseImpl) response; + StackTraceElement[] callerStackTrace = clientResponse.getCallerStackTrace(); + if (callerStackTrace != null) { + exception.setStackTrace(callerStackTrace); + } + } + + return exception; } public boolean handles(int status, MultivaluedMap headers) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientCaptureCurrentContextRestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientCaptureCurrentContextRestHandler.java new file mode 100644 index 0000000000000..383a58db4ed0a --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientCaptureCurrentContextRestHandler.java @@ -0,0 +1,49 @@ +package org.jboss.resteasy.reactive.client.handlers; + +import java.util.ArrayList; +import java.util.List; + +import org.jboss.resteasy.reactive.client.impl.ClientRequestContextImpl; +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; +import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; + +/** + * This handler is meant to be executed first in the handler chain and it captures some useful information like caller + * stacktrace. + */ +public class ClientCaptureCurrentContextRestHandler implements ClientRestHandler { + + private static final String RESTEASY_REACTIVE_PACKAGE = "org.jboss.resteasy.reactive"; + private static final String AUTOGENERATED_TAG = "$$"; + + @Override + public void handle(RestClientRequestContext requestContext) throws Exception { + ClientRequestContextImpl clientRequestContext = requestContext.getClientRequestContext(); + if (clientRequestContext == null) { + return; + } + + captureCallerStackTrace(clientRequestContext); + } + + private void captureCallerStackTrace(ClientRequestContextImpl clientRequestContext) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + List effectiveStackTrace = new ArrayList<>(stackTrace.length); + boolean foundUserMethod = false; + // skip first trace which is Thread.getStackTrace + for (int i = 1; i < stackTrace.length; i++) { + StackTraceElement trace = stackTrace[i]; + if (foundUserMethod) { + effectiveStackTrace.add(trace); + } else if (!trace.getClassName().startsWith(RESTEASY_REACTIVE_PACKAGE) + && !trace.getClassName().contains(AUTOGENERATED_TAG)) { + // Skip the latest traces that starts with the "org.jboss.resteasy.reactive" package, + effectiveStackTrace.add(trace); + foundUserMethod = true; + } + } + + clientRequestContext.getRestClientRequestContext() + .setCallerStackTrace(effectiveStackTrace.toArray(new StackTraceElement[0])); + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java index 9f72bf9af303b..95404c22b16d5 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java @@ -71,4 +71,8 @@ restClientRequestContext.properties, restClientRequestContext, getStringHeaders( public String getHttpVersion() { return restClientRequestContext.getVertxClientResponse().version().toString(); } + + public StackTraceElement[] getCallerStackTrace() { + return restClientRequestContext.getCallerStackTrace(); + } } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java index d224799962444..5be02342cb8e9 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java @@ -9,6 +9,7 @@ import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.api.LoggingScope; +import org.jboss.resteasy.reactive.client.handlers.ClientCaptureCurrentContextRestHandler; import org.jboss.resteasy.reactive.client.handlers.ClientErrorHandler; import org.jboss.resteasy.reactive.client.handlers.ClientRequestFilterRestHandler; import org.jboss.resteasy.reactive.client.handlers.ClientResponseCompleteRestHandler; @@ -26,6 +27,7 @@ class HandlerChain { private static final ClientRestHandler[] EMPTY_REST_HANDLERS = new ClientRestHandler[0]; + private final ClientRestHandler clientCaptureCurrentContextRestHandler; private final ClientRestHandler clientSwitchToRequestContextRestHandler; private final ClientRestHandler clientSendHandler; private final ClientRestHandler clientSetResponseEntityRestHandler; @@ -36,6 +38,7 @@ class HandlerChain { public HandlerChain(boolean followRedirects, LoggingScope loggingScope, Map, MultipartResponseData> multipartData, ClientLogger clientLogger) { + this.clientCaptureCurrentContextRestHandler = new ClientCaptureCurrentContextRestHandler(); this.clientSwitchToRequestContextRestHandler = new ClientSwitchToRequestContextRestHandler(); this.clientSendHandler = new ClientSendRequestHandler(followRedirects, loggingScope, clientLogger, multipartData); this.clientSetResponseEntityRestHandler = new ClientSetResponseEntityRestHandler(); @@ -52,7 +55,8 @@ ClientRestHandler[] createHandlerChain(ConfigurationImpl configuration) { List requestFilters = configuration.getRequestFilters(); List responseFilters = configuration.getResponseFilters(); if (requestFilters.isEmpty() && responseFilters.isEmpty()) { - return new ClientRestHandler[] { clientSwitchToRequestContextRestHandler, + return new ClientRestHandler[] { clientCaptureCurrentContextRestHandler, + clientSwitchToRequestContextRestHandler, clientSendHandler, clientSetResponseEntityRestHandler, clientResponseCompleteRestHandler }; @@ -65,6 +69,7 @@ ClientRestHandler[] createHandlerChain(ConfigurationImpl configuration) { for (int i = 0; i < requestFilters.size(); i++) { result.add(new ClientRequestFilterRestHandler(requestFilters.get(i))); } + result.add(clientCaptureCurrentContextRestHandler); result.add(clientSwitchToRequestContextRestHandler); result.add(clientSendHandler); result.add(clientSetResponseEntityRestHandler); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java index 3fb35e41152e7..1f2cd578af864 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java @@ -82,9 +82,6 @@ public class RestClientRequestContext extends AbstractResteasyReactiveContext, MultipartResponseData> multipartResponsesData; + private StackTraceElement[] callerStackTrace; public RestClientRequestContext(ClientImpl restClient, HttpClient httpClient, String httpMethod, URI uri, @@ -541,6 +539,14 @@ public void setMultipartResponsesData(Map, MultipartResponseData> multi this.multipartResponsesData = multipartResponsesData; } + public StackTraceElement[] getCallerStackTrace() { + return callerStackTrace; + } + + public void setCallerStackTrace(StackTraceElement[] callerStackTrace) { + this.callerStackTrace = callerStackTrace; + } + @SuppressWarnings("SameParameterValue") private Boolean getBooleanProperty(String name, Boolean defaultValue) { Object value = configuration.getProperty(name);