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

  1. Make shenyu.sync.websocket.token identical on admin and gateway configs, then restart both.
  2. Check the gateway logs/config: ensure the token is actually sent as the X-Shenyu-Sync-Token header on the upgrade request.
  3. Verify no reverse proxy strips or renames custom X- prefixed headers.
  4. 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

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

Related errors


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)