* * *
* However, the {@code keycloak-admin-client} library (built on top of JAX-RS) interprets these responses: *
* Therefore, application-level code must: *
* However, the {@code keycloak-admin-client} library (built on top of JAX-RS) interprets these responses: *
* Therefore, application-level code must: *
* However, the {@code keycloak-admin-client} library (built on top of JAX-RS) interprets these responses: *
* Therefore, application-level code must: *
/**
* Handles Keycloak REST API integration behavior and exception mapping.
*
* <p>
* Keycloak is a RESTful server and does not throw Java exceptions. Instead, it always responds to requests with:
* <ul>
* <li>An HTTP status code (e.g. 200, 401, 409, 500)</li>
* <li>Optionally, a JSON body containing an error message or metadata (e.g., {@code {"error": "User already exists"}})</li>
* </ul>
*
* <p>
* However, the {@code keycloak-admin-client} library (built on top of JAX-RS) interprets these responses:
* <ul>
* <li>If the status is 2xx (success), it returns normally.</li>
* <li>If the status is 4xx or 5xx, it throws a {@link javax.ws.rs.WebApplicationException}.</li>
* <li>If the request cannot reach the server at all (DNS failure, timeout, etc.), it throws a {@link javax.ws.rs.ProcessingException}.</li>
* </ul>
*
* <p>
* Therefore, application-level code must:
* <ul>
* <li>Catch {@link javax.ws.rs.WebApplicationException} to handle client/server-side errors (401, 403, 409, 500, etc.)</li>
* <li>Catch {@link javax.ws.rs.ProcessingException} to handle transport-level failures (e.g. {@link java.net.ConnectException}, {@link java.net.SocketTimeoutException})</li>
* <li>Log the status code and parse the response body when present</li>
* </ul>
*
* <h3>Example: Keycloak returns a 409 Conflict</h3>
*
* <pre>
* HTTP/1.1 409 Conflict
* Content-Type: application/json
*
* {
* "error": "User already exists"
* }
* </pre>
*
* <p>
* In Java:
* <pre>{@code
* try {
* UserRepresentation user = keycloak.realm("demo").users().get("xyz").toRepresentation();
* } catch (WebApplicationException e) {
* int status = e.getResponse().getStatus(); // 409
* String body = e.getResponse().readEntity(String.class); // {"error": "..."}
* // Handle or log as needed
* }
* }</pre>
*
* <h3>Summary</h3>
* <table border="1">
* <tr><th>Question</th><th>Answer</th></tr>
* <tr><td>Does Keycloak throw Java exceptions?</td><td>No</td></tr>
* <tr><td>What does Keycloak return?</td><td>HTTP status + JSON body</td></tr>
* <tr><td>Who throws Java exceptions?</td><td>The Keycloak Admin Client (JAX-RS)</td></tr>
* <tr><td>How should I handle them?</td><td>Catch WebApplicationException / ProcessingException</td></tr>
* </table>
*/
try {
// Your Keycloak admin call
}
// Network-level issues: DNS, timeout, SSL, unreachable server
catch (ProcessingException || ConnecctException || SocketTimeOutExcpetion e) {
Throwable cause = e.getCause();
if (cause instanceof ConnectException) {
log.error("Connection error: Keycloak server unreachable", cause);
} else if (cause instanceof SocketTimeoutException) {
log.error("Timeout error: Keycloak did not respond in time", cause);
} else {
log.error("Network/processing error during Keycloak call", e);
}
}
// HTTP error responses from Keycloak (4xx or 5xx)
catch (WebApplicationException e) {
Response response = e.getResponse();
int status = response.getStatus();
String body = safeReadBody(response);
if (status >= 400 && status < 500) {
switch (status) {
case 400:
log.warn("Bad request (400): {}", body);
break;
case 401:
log.warn("Unauthorized (401): token missing or expired");
break;
case 403:
log.warn("Forbidden (403): insufficient permissions");
break;
case 404:
log.warn("Not found (404): user or resource doesn't exist");
break;
case 409:
log.info("Conflict (409): resource already exists");
break;
default:
log.warn("Unhandled client error ({}): {}", status, body);
}
} else if (status >= 500) {
switch (status) {
case 500:
log.error("Internal server error (500): {}", body);
break;
case 503:
log.error("Service unavailable (503): Keycloak may be overloaded or down");
break;
default:
log.error("Unhandled server error ({}): {}", status, body);
}
} else {
log.error("Unexpected HTTP status from Keycloak ({}): {}", status, body);
}
}
// Fallback: catches anything else
catch (Exception e) {
log.error("Unexpected error during Keycloak operation", e);
}
/**
* Safely reads the body from a JAX-RS {@link Response} object.
* <p>
* This method checks whether the response has an entity before attempting to read it.
* It ensures the response body is only read once, and returns a fallback string if reading fails.
* This is useful because {@code response.readEntity(...)} is a one-time stream operation,
* and calling it multiple times or when no entity exists can throw an exception or return empty.
*
* @param response the JAX-RS Response returned by Keycloak or any REST client
* @return the response body as a String, or a safe placeholder if none is available
*/
private String safeReadBody(Response response) {
try {
if (response.hasEntity()) {
return response.readEntity(String.class);
} else {
return "<no response body>";
}
} catch (Exception e) {
return "<unable to read response body>";
}
}
// KeycloakHandler.
import lombok.extern.slf4j.Slf4j;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
/**
* Centralized handler for interpreting and logging exceptions thrown by the Keycloak Admin Client.
* <p>
* Handles both JAX-RS WebApplicationException (for HTTP errors) and ProcessingException (for transport-level errors).
*/
@Slf4j
public class KeycloakErrorHandler {
/**
* Handles HTTP-level errors (status code 4xx or 5xx) returned by Keycloak.
*
* @param e the {@link WebApplicationException} thrown by the JAX-RS client
*/
public static void handle(WebApplicationException e) {
Response response = e.getResponse();
int status = response.getStatus();
String body = safeReadBody(response);
if (status >= 400 && status < 500) {
switch (status) {
case 400: log.warn("Bad request (400): {}", body); break;
case 401: log.warn("Unauthorized (401): token missing or expired"); break;
case 403: log.warn("Forbidden (403): insufficient permissions"); break;
case 404: log.warn("Not found (404): resource not found"); break;
case 409: log.info("Conflict (409): resource already exists"); break;
case 429: log.warn("Too Many Requests (429): rate limit exceeded"); break;
default: log.warn("Unhandled client error ({}): {}", status, body);
}
} else if (status >= 500) {
switch (status) {
case 500: log.error("Internal server error (500): {}", body); break;
case 502: log.error("Bad Gateway (502): proxy failure"); break;
case 503: log.error("Service Unavailable (503): Keycloak down or restarting"); break;
case 504: log.error("Gateway Timeout (504): Keycloak did not respond"); break;
default: log.error("Unhandled server error ({}): {}", status, body);
}
} else {
log.error("Unexpected HTTP status from Keycloak ({}): {}", status, body);
}
}
/**
* Handles low-level networking errors when the HTTP request to Keycloak cannot be completed.
*
* @param e the {@link ProcessingException} thrown during transport failures
*/
public static void handle(ProcessingException e) {
Throwable cause = e.getCause();
if (cause instanceof ConnectException) {
log.error("Connection error: Keycloak server unreachable", cause);
} else if (cause instanceof SocketTimeoutException) {
log.error("Timeout error: Keycloak did not respond", cause);
} else {
log.error("Network/transport error during Keycloak call", e);
}
}
/**
* Safely reads the response body from a {@link Response} object without throwing exceptions.
* If the response has no entity or reading fails, returns a fallback message.
*
* @param response the JAX-RS Response object
* @return the response body as a string, or a safe fallback value
*/
private static String safeReadBody(Response response) {
try {
if (response.hasEntity()) {
return response.readEntity(String.class);
} else {
return "<no response body>";
}
} catch (Exception ex) {
return "<unable to read response body>";
}
}
}
// KeycloakIntegrationException.
public class KeycloakIntegrationException extends RuntimeException {
private final int statusCode;
private final String responseBody;
public KeycloakIntegrationException(int statusCode, String responseBody) {
super("Keycloak responded with status " + statusCode + ": " + responseBody);
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public int getStatusCode() {
return statusCode;
}
public String getResponseBody() {
return responseBody;
}
}