ProtocolHandshake.java
package redis.clients.jedis;
import java.util.Collections;
import redis.clients.jedis.exceptions.JedisAccessControlException;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.exceptions.JedisProtocolNotSupportedException;
/**
* Encapsulates the RESP protocol handshake logic for {@link Connection}.
* <p>
* Owns HELLO-based protocol negotiation, RESP3 fallback handling, and the recovery path for the
* Redis 6.0.x NOAUTH bug. Authentication itself remains on {@link Connection}; this class delegates
* to {@link Connection#authenticate(RedisCredentials)} where required.
* </p>
*/
final class ProtocolHandshake {
private final Connection connection;
ProtocolHandshake(Connection connection) {
this.connection = connection;
}
/**
* Establish the RESP protocol version and authenticate if needed. Performs protocol negotiation
* using the {@code HELLO} command and optional authentication, and resolves the effective RESP
* protocol used for the connection.
* <p>
* This method supports both explicit protocol selection and legacy compatibility mode.
* </p>
* <p>
* Behavior:
* </p>
* <ul>
* <li>If {@code requestedProtocol} is {@code null} and {@code autoNegotiateProtocol} is
* {@code true}, the client first attempts {@code HELLO 3} and gracefully falls back to RESP2 if
* RESP3 is not supported.</li>
* <li>If {@code requestedProtocol} is {@code null} and {@code autoNegotiateProtocol} is
* {@code false}, no {@code HELLO} is sent. The connection assumes RESP2 as the default protocol
* and only {@code AUTH} is performed if credentials are provided. This preserves the legacy
* {@link Jedis} behaviour.</li>
* <li>If {@code RESP2} is requested, a strict {@code HELLO 2} handshake is performed.</li>
* <li>If {@code RESP3} is requested, a strict {@code HELLO 3} handshake is performed.</li>
* </ul>
* @param requestedProtocol the requested RESP protocol, or {@code null} to defer to
* {@code autoNegotiateProtocol}
* @param autoNegotiateProtocol whether to attempt {@code HELLO 3} with RESP2 fallback when no
* protocol is explicitly requested; ignored when {@code requestedProtocol} is
* non-{@code null}
* @param credentials credentials used for authentication (may be {@code null})
* @return the {@link HelloResult} carrying negotiated protocol and server metadata
* @throws IllegalArgumentException if the requested protocol is not supported
* @throws JedisProtocolNotSupportedException if protocol negotiation fails
* @throws JedisDataException if the server returns an error during handshake
*/
HelloResult establish(final RedisProtocol requestedProtocol, final boolean autoNegotiateProtocol,
final RedisCredentials credentials) {
if (requestedProtocol == null) {
if (autoNegotiateProtocol) {
return negotiateResp3WithFallback(credentials);
}
// Legacy compatibility: skip HELLO entirely, only authenticate if credentials are provided.
// Connection assumes RESP2 on the wire.
connection.authenticate(credentials);
return new HelloResult(
Collections.singletonMap("proto", Long.valueOf(RedisProtocol.RESP2.version())));
} else if (requestedProtocol == RedisProtocol.RESP2) {
return enforceProtocolWithAuth(RedisProtocol.RESP2, credentials);
} else if (requestedProtocol == RedisProtocol.RESP3) {
return enforceProtocolWithAuth(RedisProtocol.RESP3, credentials);
} else {
throw new IllegalArgumentException("Unsupported protocol: " + requestedProtocol);
}
}
/**
* Send HELLO command to the server to negotiate the protocol version and authenticate if needed.
* <p>
* Attempts RESP3 handshake, falls back to RESP2 if not supported.
* </p>
* @param credentials credentials for authentication
* @return {@link HelloResult} the actual negotiated protocol version
*/
private HelloResult negotiateResp3WithFallback(final RedisCredentials credentials) {
try {
return enforceProtocolWithAuth(RedisProtocol.RESP3, credentials);
} catch (JedisProtocolNotSupportedException e) {
// fall back to resp2
return establishLegacyResp2(credentials);
} catch (JedisDataException e) {
// fall back to resp2
if (isUnknownCommandError(e)) {
return establishLegacyResp2(credentials);
}
throw e;
}
}
/**
* Performs strict protocol negotiation using the {@code HELLO} command.
* <p>
* This method enforces the provided {@code protocol} version and expects the server to support
* the {@code HELLO} command. It does not perform protocol fallback (e.g., RESP3 ��� RESP2).
* </p>
* <p>
* Behavior:
* </p>
* <ul>
* <li>Attempts negotiation via {@code HELLO <protocol> AUTH <user> <pass>} in a single command,
* sending credentials inline to avoid an extra round-trip on Redis 6.2.2+.</li>
* <li>If the server rejects the request with a NOAUTH error (observed in Redis 6.0.x prior to
* 6.2.2, where {@code HELLO AUTH} is not honored), falls back to a standalone {@code AUTH}
* followed by a retry of {@code HELLO} with the same credentials.</li>
* <li>Any non-authentication-related errors are propagated to the caller.</li>
* </ul>
* <p>
* Notes:
* </p>
* <ul>
* <li>Some Redis 6.0.x versions require authentication before allowing {@code HELLO}, even though
* {@code HELLO AUTH} is supported in later versions.</li>
* <li>This method assumes the server supports the requested protocol; unsupported protocol errors
* are not handled and will be propagated.</li>
* </ul>
* @param protocol the RESP protocol version to negotiate (must not be {@code null})
* @param credentials credentials used for authentication if required (may be {@code null})
* @return the {@code HELLO} response containing negotiated protocol and server metadata
* @throws IllegalArgumentException if {@code protocol} is {@code null}
* @throws JedisProtocolNotSupportedException if the server does not support the requested
* protocol
* @throws JedisAccessControlException if authentication fails and cannot be recovered
*/
private HelloResult enforceProtocolWithAuth(RedisProtocol protocol,
RedisCredentials credentials) {
if (protocol == null) {
throw new IllegalArgumentException("protocol must not be null");
}
try {
try {
return connection.hello(protocol, credentials);
} catch (JedisDataException e) {
if (isUnknownCommandError(e)) {
throw new JedisProtocolNotSupportedException("Server does not support HELLO", e);
} else {
throw e;
}
}
} catch (JedisAccessControlException e) {
// Redis 6.0.x (before 6.2.2) has a bug where HELLO with AUTH fails if the default user
// requires authentication ��� the server demands AUTH before allowing HELLO.
// See: https://github.com/redis/redis/issues/8558
// See: https://github.com/redis/lettuce/issues/2592
if (isNoAuthError(e)) {
connection.authenticate(credentials);
return connection.hello(protocol, credentials);
} else {
throw e;
}
}
}
/**
* Fallback handshake used when RESP3 or {@code HELLO} is not supported by the server.
* <p>
* This method provides compatibility with legacy Redis servers that do not support the
* {@code HELLO} command.
* </p>
* <p>
* Behavior:
* </p>
* <ul>
* <li>Performs {@code AUTH} if credentials are provided.</li>
* <li>Attempts a {@code HELLO 2} command to retrieve server metadata.</li>
* <li>If the server does not support {@code HELLO}, assumes RESP2 as the default protocol.</li>
* </ul>
* <p>
* Fallback logic:
* </p>
* <ul>
* <li>If {@code HELLO} succeeds ��� uses returned protocol and metadata.</li>
* <li>If {@code HELLO} fails with unknown command ��� assumes RESP2 and continues.</li>
* <li>Any other errors are propagated to the caller.</li>
* </ul>
* <p>
* Note:
* </p>
* <ul>
* <li>This method exists solely for backward compatibility with Redis versions prior to 6.0.</li>
* <li>Server version and protocol information may be incomplete when fallback is used.</li>
* </ul>
* @param credentials credentials used for authentication (may be {@code null})
* @return {@link HelloResult} containing protocol and server metadata (may be inferred for legacy
* servers)
* @throws JedisDataException if a non-recoverable server error occurs
*/
private HelloResult establishLegacyResp2(final RedisCredentials credentials) {
// authenticate first to support legacy behavior on server not supporting HELLO
connection.authenticate(credentials);
try {
return connection.hello(RedisProtocol.RESP2, null);
} catch (JedisDataException e) {
// if server does not support hello, we assume RESP2
if (isUnknownCommandError(e)) {
return new HelloResult(
Collections.singletonMap("proto", Long.valueOf(RedisProtocol.RESP2.version())));
}
throw e;
}
}
static boolean isNoAuthError(JedisDataException e) {
return e.getMessage().startsWith("NOAUTH");
}
static boolean isUnknownCommandError(JedisDataException e) {
return e.getMessage().startsWith("ERR") && e.getMessage().contains("unknown command");
}
}