quarkus icon indicating copy to clipboard operation
quarkus copied to clipboard

Keycloak admin client - error response body is unreachable

Open majugurci opened this issue 1 year ago • 9 comments

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

majugurci avatar Aug 15 '24 20:08 majugurci

/cc @pedroigor (keycloak), @sberyozkin (keycloak)

quarkus-bot[bot] avatar Aug 15 '24 20:08 quarkus-bot[bot]

@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 avatar Aug 15 '24 20:08 sberyozkin

@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?

majugurci avatar Aug 16 '24 09:08 majugurci

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

geoand avatar Aug 26 '24 06:08 geoand

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.

majugurci avatar Aug 26 '24 08:08 majugurci

Could this be made configurable?

It's the server that does not return the data containing the error message

geoand avatar Aug 26 '24 08:08 geoand

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.

majugurci avatar Aug 26 '24 08:08 majugurci

Ah okay, then I'll leave this to @sberyozkin to explore options on the client part

geoand avatar Aug 26 '24 08:08 geoand

@geoand Maybe this issue shouldn't be closed if you are planning to fix it?

majugurci avatar Aug 28 '24 07:08 majugurci

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.. Image.

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 avatar Jan 17 '25 07:01 Postremus

@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 avatar Jan 17 '25 08:01 majugurci

@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 :)

Postremus avatar Jan 20 '25 08:01 Postremus

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

sbaeumlisberger avatar Jun 26 '25 08:06 sbaeumlisberger

Seems to be fixed in version 3.22 (92f09f23078cb85754055be9857720a9370efc66)

sbaeumlisberger avatar Jul 09 '25 13:07 sbaeumlisberger

Seems to be fixed in version 3.22 (92f09f2)

Can confirm, after upgrading Quarkus it is now working.

majugurci avatar Aug 13 '25 13:08 majugurci