Why Does Keycloak PublicKey Retrieval Hang in Spring Boot? Timeout Fixes Explained
This article analyzes intermittent page failures caused by blocked Keycloak public‑key retrieval in a Spring Boot application, explains how default HTTP client timeouts of –1 lead to indefinite waits, and provides a filter‑based solution to set proper timeouts and adjust internal/external URLs.
Problem Description
The project uses Keycloak for unified authentication with a Spring Boot backend. Occasionally the page becomes inaccessible, then recovers after a while. Thread dump shows a blocked thread waiting on
org.keycloak.adapters.rotation.JWKPublicKeyLocator.getPublicKey:
"http-nio-8081-exec-9" #61 daemon prio=5 os_prio=0 tid=0x00007efc702c1000 nid=0x4c waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at org.keycloak.adapters.rotation.JWKPublicKeyLocator.getPublicKey(JWKPublicKeyLocator.java:60)
- waiting to lock <0x00000003f1888968> (a org.keycloak.adapters.rotation.JWKPublicKeyLocator)
...The blockage occurs when the singleton JWKPublicKeyLocator holds a synchronized lock while fetching the public key from Keycloak.
Root Cause Analysis
The JWKPublicKeyLocator synchronizes the request, and the underlying HTTP client uses Apache HttpClient with default RequestConfig values of -1 for connectionRequestTimeout, connectTimeout, and socketTimeout. A value of -1 means no timeout, so a slow or failed network call blocks the lock indefinitely, causing other threads to wait.
Keycloak configuration in application.properties provides only the base URL:
keycloak.realm=realmId
keycloak.resource=clientId
keycloak.auth-server-url=http://127.0.0.1:8180/authWhen the external network is unstable, DNS resolution and load‑balancing add latency, and the default timeout settings prevent the request from failing fast.
Solution
Two main actions are required:
Set explicit timeouts for the HTTP client. A custom Spring filter modifies the global KeycloakDeployment client parameters:
@Component
public class ChangeTimeOutFilter implements Filter {
@Resource
private AdapterDeploymentContext deploymentContext;
private volatile boolean deploymentChanged = false;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpFacade facade = new SimpleHttpFacade((HttpServletRequest) request, (HttpServletResponse) response);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (deployment == null) { chain.doFilter(request, response); return; }
if (deploymentChanged) { chain.doFilter(request, response); return; }
HttpParams params = deployment.getClient().getParams();
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 10000);
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, 10000L);
deploymentChanged = true;
chain.doFilter(request, response);
}
}Testing with a very short timeout produces a clear SocketTimeoutException, confirming the configuration works.
Use internal IP for Keycloak calls while returning the external URL to the front‑end. Change keycloak.auth-server-url to the internal address (e.g., http://172.17.0.1:8180/auth) and add a separate property for the external URL that is only sent to the browser.
If the front‑end receives a different realm URL than the one stored in the deployment, token verification fails with a RealmUrlCheck error. To align them, a second filter rewrites the realmInfoUrl field via reflection:
@Component
public class ChangeTimeOutFilter implements Filter {
@Resource
private AdapterDeploymentContext deploymentContext;
@Resource
private KeyCloakConfig keyCloakConfig;
private static Field realmInfoUrlFd;
static {
try { realmInfoUrlFd = KeycloakDeployment.class.getDeclaredField("realmInfoUrl"); realmInfoUrlFd.setAccessible(true); }
catch (Exception ignored) {}
}
private volatile boolean deploymentChanged = false;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpFacade facade = new SimpleHttpFacade((HttpServletRequest) request, (HttpServletResponse) response);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (deployment == null) { chain.doFilter(request, response); return; }
if (!deploymentChanged) {
String realmInfoUrl = deployment.getRealmInfoUrl();
if (!StringUtils.isBlank(realmInfoUrl)) {
realmInfoUrl = realmInfoUrl.replaceAll(keyCloakConfig.getInnerUrl(), keyCloakConfig.getAuthUrl());
try { realmInfoUrlFd.set(deployment, realmInfoUrl); } catch (Exception ignored) {}
}
HttpParams params = deployment.getClient().getParams();
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 10000);
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, 10000L);
deploymentChanged = true;
}
chain.doFilter(request, response);
}
}After applying these changes, the intermittent “page cannot open” issue disappears.
Future posts will continue to share similar troubleshooting experiences from the Guandata technical team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
