TL;DR
Server-side credential‑rejection failures (wrong password, invalid JWT/key‑pair, invalid OAuth token) that reach the login‑failure throw site are thrown as a SnowflakeSQLException with SQLState 08001 ("SQL‑client unable to establish SQL‑connection"). The driver hardcodes this state at that throw site regardless of the server's error code. The correct SQLState for these is 28000 ("invalid authorization specification") — which the driver already uses for locally‑detected missing credentials. Any tooling that classifies errors by SQLState (jOOQ, connection pools, retry frameworks) therefore treats a bad password as a transient connection outage and retries it / surfaces the wrong status.
1. What version of the JDBC driver are you using?
net.snowflake:snowflake-jdbc:4.3.1 (current latest on Maven Central; verified at runtime). The login‑failure throw site described below is present on current master (checked at v4.3.1+).
2. What operating system and processor architecture?
macOS / aarch64. The defect is platform‑independent — it is pure server‑response‑parsing logic.
3. What version of Java are you using?
Java 21.0.11.
4. What did you do? (reproduction)
Connect to a real account with bad credentials and inspect the raw exception. Minimal runnable repro:
import java.sql.*;
import java.util.Properties;
public class SnowflakeAuthSqlStateRepro {
public static void main(String[] args) {
String url = "jdbc:snowflake://<account>.snowflakecomputing.com/";
Properties p = new Properties();
p.put("user", "<real-existing-user>");
p.put("password", "definitely-the-wrong-password"); // real user, wrong password
try (Connection c = DriverManager.getConnection(url, p)) {
System.out.println("unexpected success");
} catch (SQLException e) {
System.out.printf("errorCode=%d sqlState=%s msg=%s%n",
e.getErrorCode(), e.getSQLState(), e.getMessage());
}
}
}
Observed output (wrong password):
errorCode=390100 sqlState=08001 msg=Incorrect username or password was specified.
Observed output (key‑pair auth with an unregistered key, authenticator=SNOWFLAKE_JWT):
errorCode=390144 sqlState=08001 msg=JWT token is invalid. [<requestId>]
5. What did you expect to see?
For an authentication failure, the SQLState should be 28000 (INVALID_AUTHORIZATION_SPECIFICATION), not the connection class 08001. SQLState class 28 is the SQL‑standard "invalid authorization specification"; class 08 means the client could not establish a connection — which is misleading when the connection was established and the server actively rejected the credentials.
6. DEBUG logs — the raw server login response
With net.snowflake logging at DEBUG, the verbatim login‑failure response body is:
{
"responseType" : "LoginResponse",
"data" : {
"nextAction" : "RETRY_LOGIN",
"authnMethod" : "USERNAME_PASSWORD",
"loginName" : "<USER>",
"signInOptions" : { }
},
"success" : false,
"message" : "Incorrect username or password was specified.",
"code" : "390100"
}
Note: the response contains no sqlState field — only the envelope code. So the SQLState is chosen entirely by the driver.
Root cause
src/main/java/net/snowflake/client/internal/core/SessionUtil.java — the login‑failure handler unconditionally stamps the connection SQLState, ignoring the server code:
// ~line 940, after reading code/message from the response envelope
throw new SnowflakeSQLException(
NO_QUERY_ID,
errorMessage,
SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION, // 08001 — hardcoded for ALL login failures
errorCode); // 390100 / 390144 / ... from the server
The same root cause also affects the external‑browser login flow in SessionUtilExternalBrowser.java (~line 215). That site uses a String error code and a different SnowflakeSQLException constructor, so the fix there is equivalent but not identical to the snippet below.
The inconsistency this creates
The driver already maps locally-detected credential problems to 28000 (ErrorCode.java):
MISSING_USERNAME(200011, SqlState.INVALID_AUTHORIZATION_SPECIFICATION), // 28000
MISSING_PASSWORD(200012, SqlState.INVALID_AUTHORIZATION_SPECIFICATION), // 28000
So a missing password yields 28000, but a wrong password (server‑rejected) yields 08001. Same category of problem, two different SQLState classes. (28000 is the driver's established convention for authorization‑input problems — it is also used by MISSING_SERVER_URL (200026), MISSING_CONNECTION_PROPERTY (200028) and INVALID_CONNECTION_URL (200029).)
Why it matters
SQLState is the portable, vendor‑neutral classifier that generic tooling relies on. With 08001, an authentication failure is indistinguishable from a real connectivity outage, so consumers:
- retry a permanent auth failure as if it were transient (connection pools, retry frameworks), and
- surface the wrong status (e.g. jOOQ maps
08001 → C08_CONNECTION_EXCEPTION; downstream services translate that to 503 Service Unavailable rather than 401/Unauthenticated).
Proposed fix
At the login‑failure throw site, select the SQLState from the server code for known credential‑rejection codes; otherwise keep 08001 unchanged (no regression for non‑auth failures):
private static final Set<Integer> AUTH_REJECTION_GS_CODES = Set.of(
390100, // INCORRECT_USERNAME_PASSWORD — reproduced firsthand
390144, // JWT_TOKEN_INVALID — reproduced firsthand
// The JWT codes below are candidates derived from Snowflake docs; please confirm:
394300, // JWT_TOKEN_INVALID_USER_IN_ISSUER (candidate)
394301, // JWT_TOKEN_MISSING_ISSUE_OR_EXPIRATION_TIME (candidate)
394304, // JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH (candidate)
394305, // JWT_TOKEN_INVALID_ALGORITHM (candidate)
394306, // JWT_TOKEN_INVALID_SIGNATURE (candidate)
390303 // OAUTH_ACCESS_TOKEN_INVALID (Constants.java)
);
String sqlState = AUTH_REJECTION_GS_CODES.contains(errorCode)
? SqlState.INVALID_AUTHORIZATION_SPECIFICATION // 28000
: SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION; // 08001 (default, unchanged)
throw new SnowflakeSQLException(NO_QUERY_ID, errorMessage, sqlState, errorCode);
SqlState.INVALID_AUTHORIZATION_SPECIFICATION already exists (in net.snowflake:snowflake-common) and is already used by the driver, so no new dependency or constant is required.
- An equivalent change applies at the external‑browser site (
SessionUtilExternalBrowser.java ~line 215), adjusted for its String error code and its different SnowflakeSQLException constructor (arg order / Integer.valueOf).
- Non‑auth failures keep
08001, so genuine connectivity errors are unaffected.
Codes deliberately excluded (not credential rejection)
390318 OAUTH_ACCESS_TOKEN_EXPIRED — diverted before this throw site (re‑auth path; its non‑re‑auth branch currently surfaces an INTERNAL_ERROR SQLState — a separate matter).
390111 / 390112 session gone / expired, 390195 id‑token invalid — session/token lifecycle, handled on other paths.
Compatibility
This changes only the SQLState string for the listed credential‑rejection codes; the vendor error code returned by getErrorCode() (e.g. 390100) is unchanged, so any consumer keying on the error code keeps working. It is a runtime value, not an API signature — no binary/japicmp break. The one behavioral change is that consumers currently treating 08001 as a transient connection error will now (correctly) see 28000 for these auth failures.
Question for maintainers
Is the list above the complete and correct set of credential‑rejection GS codes (390xxx / 394xxx)? You hold the authoritative error‑code catalog; we derived this set from the docs plus firsthand reproduction of 390100 and 390144. Happy to adjust the set and open the PR.
References
TL;DR
Server-side credential‑rejection failures (wrong password, invalid JWT/key‑pair, invalid OAuth token) that reach the login‑failure throw site are thrown as a
SnowflakeSQLExceptionwith SQLState08001("SQL‑client unable to establish SQL‑connection"). The driver hardcodes this state at that throw site regardless of the server's errorcode. The correct SQLState for these is28000("invalid authorization specification") — which the driver already uses for locally‑detected missing credentials. Any tooling that classifies errors by SQLState (jOOQ, connection pools, retry frameworks) therefore treats a bad password as a transient connection outage and retries it / surfaces the wrong status.1. What version of the JDBC driver are you using?
net.snowflake:snowflake-jdbc:4.3.1(current latest on Maven Central; verified at runtime). The login‑failure throw site described below is present on currentmaster(checked atv4.3.1+).2. What operating system and processor architecture?
macOS /
aarch64. The defect is platform‑independent — it is pure server‑response‑parsing logic.3. What version of Java are you using?
Java
21.0.11.4. What did you do? (reproduction)
Connect to a real account with bad credentials and inspect the raw exception. Minimal runnable repro:
Observed output (wrong password):
Observed output (key‑pair auth with an unregistered key,
authenticator=SNOWFLAKE_JWT):5. What did you expect to see?
For an authentication failure, the SQLState should be
28000(INVALID_AUTHORIZATION_SPECIFICATION), not the connection class08001. SQLState class28is the SQL‑standard "invalid authorization specification"; class08means the client could not establish a connection — which is misleading when the connection was established and the server actively rejected the credentials.6. DEBUG logs — the raw server login response
With
net.snowflakelogging at DEBUG, the verbatim login‑failure response body is:{ "responseType" : "LoginResponse", "data" : { "nextAction" : "RETRY_LOGIN", "authnMethod" : "USERNAME_PASSWORD", "loginName" : "<USER>", "signInOptions" : { } }, "success" : false, "message" : "Incorrect username or password was specified.", "code" : "390100" }Note: the response contains no
sqlStatefield — only the envelopecode. So the SQLState is chosen entirely by the driver.Root cause
src/main/java/net/snowflake/client/internal/core/SessionUtil.java— the login‑failure handler unconditionally stamps the connection SQLState, ignoring the servercode:The same root cause also affects the external‑browser login flow in
SessionUtilExternalBrowser.java(~line 215). That site uses aStringerror code and a differentSnowflakeSQLExceptionconstructor, so the fix there is equivalent but not identical to the snippet below.The inconsistency this creates
The driver already maps locally-detected credential problems to
28000(ErrorCode.java):So a missing password yields
28000, but a wrong password (server‑rejected) yields08001. Same category of problem, two different SQLState classes. (28000is the driver's established convention for authorization‑input problems — it is also used byMISSING_SERVER_URL(200026),MISSING_CONNECTION_PROPERTY(200028) andINVALID_CONNECTION_URL(200029).)Why it matters
SQLState is the portable, vendor‑neutral classifier that generic tooling relies on. With
08001, an authentication failure is indistinguishable from a real connectivity outage, so consumers:08001→C08_CONNECTION_EXCEPTION; downstream services translate that to 503 Service Unavailable rather than 401/Unauthenticated).Proposed fix
At the login‑failure throw site, select the SQLState from the server
codefor known credential‑rejection codes; otherwise keep08001unchanged (no regression for non‑auth failures):SqlState.INVALID_AUTHORIZATION_SPECIFICATIONalready exists (innet.snowflake:snowflake-common) and is already used by the driver, so no new dependency or constant is required.SessionUtilExternalBrowser.java~line 215), adjusted for itsStringerror code and its differentSnowflakeSQLExceptionconstructor (arg order /Integer.valueOf).08001, so genuine connectivity errors are unaffected.Codes deliberately excluded (not credential rejection)
390318OAUTH_ACCESS_TOKEN_EXPIRED — diverted before this throw site (re‑auth path; its non‑re‑auth branch currently surfaces anINTERNAL_ERRORSQLState — a separate matter).390111/390112session gone / expired,390195id‑token invalid — session/token lifecycle, handled on other paths.Compatibility
This changes only the SQLState string for the listed credential‑rejection codes; the vendor error code returned by
getErrorCode()(e.g.390100) is unchanged, so any consumer keying on the error code keeps working. It is a runtime value, not an API signature — no binary/japicmpbreak. The one behavioral change is that consumers currently treating08001as a transient connection error will now (correctly) see28000for these auth failures.Question for maintainers
Is the list above the complete and correct set of credential‑rejection GS codes (
390xxx/394xxx)? You hold the authoritative error‑code catalog; we derived this set from the docs plus firsthand reproduction of390100and390144. Happy to adjust the set and open the PR.References
390144,394300–394307): https://docs.snowflake.com/en/user-guide/key-pair-auth-troubleshooting