apache/shenyu · error · ShenyuException
websocket sync token is invalid
Error message
websocket sync token is invalid
What it means
During the websocket handshake, checkSyncToken compares the client's X-Shenyu-Sync-Token header to the admin's configured shenyu.sync.websocket.token using a constant-time comparison. If the header is missing/blank or the value differs, it throws ShenyuException and the handshake is rejected. This is an authentication failure, not a network problem.
Solutions
- Make shenyu.sync.websocket.token identical on admin and gateway configs, then restart both.
- Check the gateway logs/config: ensure the token is actually sent as the X-Shenyu-Sync-Token header on the upgrade request.
- Verify no reverse proxy strips or renames custom X- prefixed headers.
- Compare raw values (e.g. echo the config) to catch invisible whitespace or quote mismatches after a secret rotation.
Example fix
// before (gateway)
shenyu:
sync:
websocket:
token: "oldSecret"
// after (must match admin)
shenyu:
sync:
websocket:
token: "mySharedSecret" Defensive patterns
Strategy: validation
Validate before calling
String adminToken = config.getAdminToken();
String gatewayToken = config.getGatewayToken();
if (gatewayToken == null || !MessageDigest.isEqual(adminToken.getBytes(), gatewayToken.getBytes()))
throw new IllegalStateException("websocket sync token mismatch between admin and gateway"); Try / catch
try {
handshake();
} catch (ShenyuException e) {
if (e.getMessage().contains("token is invalid"))
LOG.error("sync token mismatch — compare shenyu.sync.websocket.token on both sides");
} Prevention
- Keep admin and gateway tokens in one source of truth (shared secret template)
- Rotate the token on both sides atomically
- Confirm reverse proxies forward the X-Shenyu-Sync-Token header
- Watch for YAML quoting/whitespace altering the token string
When it happens
Trigger: Gateway connects to ws://admin:9095/websocket with a missing X-Shenyu-Sync-Token header, an empty token, or a token value that does not exactly equal the admin's configured token (case/whitespace differences count).
Common situations: Token set on admin but not on the gateway (or vice versa); values differ after rotating the secret on one side only; YAML quoting/whitespace causing one side to read a different string; gateway behind a proxy that strips custom X- headers.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- websocket sync token is not configured
- websocket on client open failed, namespaceId is null
- namespaceId can not be null
- is not allowed.
- userName is null
AI-assisted analysis of apache/shenyu@567142e072 (2026-09-12).
Data as JSON: /api/errors/f3f1ad18abd42f7a.
Report an issue: GitHub.
Appendix: source
Thrown at shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketConfigurator.java:105
@Override
public void onStartup(final ServletContext servletContext) {
int messageMaxSize = getWebsocketSyncProperties().getMessageMaxSize();
if (messageMaxSize > 0) {
servletContext.setInitParameter(TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM,
String.valueOf(messageMaxSize));
servletContext.setInitParameter(BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM,
String.valueOf(messageMaxSize));
}
}
private void checkSyncToken(final HandshakeRequest request) {
String configuredToken = getWebsocketSyncProperties().getToken();
if (StringUtils.isBlank(configuredToken)) {
throw new ShenyuException("websocket sync token is not configured");
}
String requestToken = getHeader(request.getHeaders(), Constants.X_SHENYU_SYNC_TOKEN);
if (StringUtils.isBlank(requestToken) || !isSameToken(configuredToken, requestToken)) {
throw new ShenyuException("websocket sync token is invalid");
}
}
private WebsocketSyncProperties getWebsocketSyncProperties() {
return Optional.ofNullable(websocketSyncProperties)
.orElseGet(() -> SpringBeanUtils.getInstance().getBean(WebsocketSyncProperties.class));
}
private boolean isSameToken(final String configuredToken, final String requestToken) {
return MessageDigest.isEqual(
configuredToken.getBytes(StandardCharsets.UTF_8),
requestToken.getBytes(StandardCharsets.UTF_8));
}
private String getHeader(final Map<String, List<String>> headers, final String name) {
return Optional.ofNullable(headers)
.orElse(Collections.emptyMap())
.entrySet()View on GitHub (pinned to 567142e072)