Keycloak admin client - error response body is unreachable
Describe the bug
When using Quarkus with dependency quarkus-keycloak-admin-client-reactive and invoking some method which does not return Response how do I get real reason why invocation has failed?
For example, if I invoke a method to update user:
UserResource userResource = fetchUserById(userId); userResource.update(user);
I get the following exception stack trace:
ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /rest/v1/change-username failed, error id: d149a840-21bb-454f-97d9-50af5dc70359-1: org.jboss.resteasy.reactive.ClientWebApplicationException: Received: 'Server response is: 400' when invoking: Rest Client method: 'org.keycloak.admin.client.resource.UserResource#update'
at org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.unwrapException(RestClientRequestContext.java:195)
at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.handleException(AbstractResteasyReactiveContext.java:331)
at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:175)
at org.jboss.resteasy.reactive.client.impl.RestClientRequestContext$1.lambda$execute$0(RestClientRequestContext.java:314)
at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:279)
at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:261)
at io.vertx.core.impl.ContextInternal.lambda$runOnContext$0(ContextInternal.java:59)
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:166)
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:840)
Caused by: org.jboss.resteasy.reactive.client.api.WebClientApplicationException: Server response is: 400
at org.jboss.resteasy.reactive.client.handlers.ClientSetResponseEntityRestHandler.handle(ClientSetResponseEntityRestHandler.java:32)
at org.jboss.resteasy.reactive.client.handlers.ClientSetResponseEntityRestHandler.handle(ClientSetResponseEntityRestHandler.java:23)
at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.invokeHandler(AbstractResteasyReactiveContext.java:231)
at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
... 12 more
Trying to get entity from ClientWebApplicationException or WebApplicationException is giving me null.
But if I turn on the debug log for http request:
quarkus.rest-client.logging.scope=request-response
quarkus.rest-client.logging.body-limit=1024
quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG
I can see that http response has a body attached with a detail why request has failed. It's just that is is not mapped to an exception. For example:
DEBUG [org.jbo.res.rea.cli.log.DefaultClientLogger] (vert.x-eventloop-thread-0) Response: PUT https://keycloak-instance/admin/realms/realmName/users/da71b24c-eb88-4147-85a6-b172a093958f, Status[400 Bad Request], Headers[Date=Wed, 14 Aug 2024 20:35:50 GMT Content-Type=application/json Content-Length=92 Connection=keep-alive Referrer-Policy=no-referrer Strict-Transport-Security=max-age=31536000; includeSubDomains X-Content-Type-Options=nosniff X-Frame-Options=SAMEORIGIN X-XSS-Protection=1; mode=block], Body:
{"field":"username","errorMessage":"error-username-invalid-character","params":["username"]}
But if I create a user:
Response response = keycloak.realm("realmName").users().create(user);
I can read the entity from response which gives me reason why request have failed.
Does anyone know how to get response body when doing the user update?
I have tried implementing custom exception mappers but guess they don't work because it is handled by rest client.
Expected behavior
No response
Actual behavior
No response
How to Reproduce?
No response
Output of uname -a or ver
No response
Output of java -version
No response
Quarkus version or git rev
No response
Build tool (ie. output of mvnw --version or gradlew --version)
No response
Additional information
No response
/cc @pedroigor (keycloak), @sberyozkin (keycloak)
@majugurci May be ClientResponseFilter can help ? In general, as far as I know, the error body is not returned with exceptions to avoid it being leaked by accident back to the client of the Quarkus application which makes a REST client call.
Also CC @geoand
@sberyozkin I have tried with ClientResponseFilter and custom exception mapper but it seems it is not used with keycloak rest client.
If I register ClientResponseFilter or custom exception mapper with @ Provider annotation than it's methods are called for rest clients which I have in my application but not for the keycloak rest client.
I have also tried registering it through application.properties like this but it didn't help:
org.keycloak.admin.client.spi.ResteasyClientProvider/mp-rest/providers=org.filters.MyCustomFilter
or this
io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyClientProvider/mp-rest/providers=org.filters.MyCustomFilter
EDIT: I guess it is related to this issue: https://github.com/quarkusio/quarkus/discussions/33332 Is there any other workaround I could try to get response body?
In general, as far as I know, the error body is not returned with exceptions to avoid it being leaked by accident back to the client of the Quarkus application which makes a REST client call. Also CC @geoand
Exactly correct
In general, as far as I know, the error body is not returned with exceptions to avoid it being leaked by accident back to the client of the Quarkus application which makes a REST client call. Also CC @geoand
Exactly correct
Could this be made configurable?
We have a use case where we're managing users in our web app through Keycloak admin api. If there is an error while updating a user there is no error explanation so we can only show "Unknown error" in our web application. As you can see that is not really helpful in case of an error like username contains invalid characters or email already in use.
Could this be made configurable?
It's the server that does not return the data containing the error message
It's the server that does not return the data containing the error message
By the server you mean the Keycloak server? If that is so I can confirm that Keycloak server is returning the error, I have attached debug log in original post.
Ah okay, then I'll leave this to @sberyozkin to explore options on the client part
@geoand Maybe this issue shouldn't be closed if you are planning to fix it?
Hey @geoand I am now facing the same problem.
https://github.com/quarkusio/quarkus/blob/main/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/WebClientApplicationException.java#L29
The used Exception simply does not ever have the reponse available, just a dummy implementation. So it is more of a general quarkus-rest-client problem, not specific to keycloak.
i.e. bufferEntity on the response always fails, readEntity(String.class) also fails, etc..
.
Fully implementing the response in the exception would at least allow to catch the exception, and act on the response. No idea if exception mappers already work with keycloak-admin-client, but at least we could work around the problem.
@Postremus If it is of any help I have managed to work around this issue by calling problematic methods directly through RestClient. So now I use this extension for methods where you can get an exception details and RestClient for methods where this extension does not return exception details.
RestClient:
@Dependent
@OidcClientFilter
@RegisterRestClient(configKey = "keycloak.admin.client.url")
public interface KeycloakRestClient {
@PUT
@Path("users/{id}")
void updateUser(@RestPath String id, UserRepresentation user);
@PUT
@Path("users/{id}/reset-password")
void resetPassword(@RestPath String id, CredentialRepresentation credentialRepresentation);
}
RestClient invokation:
try {
keycloakRestClient.get().updateUser(user.getId(), user);
} catch (Exception e1) {
try {
WebApplicationException wae = (WebApplicationException) e1.getCause();
Response response = wae.getResponse();
if (response.getStatus() >= 400) {
KeycloakErrorRepresentation errorRepresentation = response
.readEntity(KeycloakErrorRepresentation.class);
String field = errorRepresentation.getField();
String errorMessage = errorRepresentation.getErrorMessage();
if (field != null && field.equals("username")) {
if (errorMessage != null) {
if (errorMessage.equals("error-invalid-length")) {
// handle error
} else if (errorMessage.equals("error-username-invalid-character")) {
// handle error
}
}
}
String error = errorRepresentation.getError();
if (error != null && error.equals("invalidPasswordBlacklistedMessage")) {
// handle error
}
String finalError = errorMessage != null ? errorMessage : error;
throw new WebApplicationException(finalError);
}
} catch (Exception e2) {
if (e2 instanceof WebApplicationException || e2 instanceof ApiValidationException) {
throw e2;
} else {
e1.printStackTrace();
e2.printStackTrace();
throw new InternalServerErrorException();
}
}
@majugurci Thanks for the idea.
In the end this was my final solution:
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/admin/realms/{realm}/users/{user-id}")
public interface UserAPI extends Closeable {
@PUT
@Path("reset-password")
Response resetPassword(@PathParam("realm") String realm, @PathParam("user-id") String userId, CredentialRepresentation credentialRepresentation);
}
@Inject
Keycloak keycloak;
@ConfigProperty(name = "quarkus.keycloak.admin-client.server-url")
URI keycloakAdminServerUrl;
[....]
try (UserAPI userAPI = keycloak.proxy(UserAPI.class, keycloakAdminServerUrl)) {
Response response = userAPI.resetPassword([....]);
if (response.getStatus() >= 400) {
[....]
}
}
Still, would be nicer if ClientWebApplicationException provided us with the Response object :)
We're having the same problem. We are currently migrating to Quarkus and have used the response to provide meaningful error messages to users and implement fallback behavior. Do you have any updates on when this will be fixed? @geoand @sberyozkin
Seems to be fixed in version 3.22 (92f09f23078cb85754055be9857720a9370efc66)