Repository: microsoft/mssql-jdbc
Pull request #2355 (2024-03-19 19:31:42)

socketTimeout should reset to original value after a successful connection open
https://github.com/microsoft/mssql-jdbc/issues/2354

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2355?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) Report
Attention: Patch coverage is `33.33333%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 49.98%. Comparing base [(`00a2556`)](https://app.codecov.io/gh/microsoft/mssql-jdbc/commit/00a25560ba95590eb9ca181286024ba0cf889d07?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) to head [(`bc11772`)](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2355?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft).

> :exclamation: Current head bc11772 differs from pull request most recent head a479ec0. Consider uploading reports for the commit a479ec0 to get more accurate results

| [Files](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2355?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) | Patch % | Lines |
|---|---|---|
| [.../microsoft/sqlserver/jdbc/SQLServerConnection.java](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2355?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft#diff-c3JjL21haW4vamF2YS9jb20vbWljcm9zb2Z0L3NxbHNlcnZlci9qZGJjL1NRTFNlcnZlckNvbm5lY3Rpb24uamF2YQ==) | 33.33% | [2 Missing :warning: ](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2355?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main    #2355      +/-   ##
============================================
- Coverage     50.00%   49.98%   -0.03%     
- Complexity     3787     3791       +4     
============================================
  Files           143      142       -1     
  Lines         33186    33170      -16     
  Branches       5631     5624       -7     
============================================
- Hits          16596    16580      -16     
- Misses        14202    14229      +27     
+ Partials       2388     2361      -27     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/microsoft/mssql-jdbc/pull/2355?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft).


Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index 3a5db2bf..afde3910 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -2366,6 +2366,10 @@ final class TDSChannel implements Serializable {
     final void setNetworkTimeout(int timeout) throws IOException {
         tcpSocket.setSoTimeout(timeout);
     }
+
+    void resetTcpSocketTimeout() throws SocketException {
+        this.tcpSocket.setSoTimeout(con.getSocketTimeoutMilliseconds());
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index 2f54a98a..2479e506 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -483,6 +483,25 @@ public interface ISQLServerConnection extends java.sql.Connection {
      */
     void setAccessTokenCallbackClass(String accessTokenCallbackClass);
 
+    /**
+     * Get Currently installed message handler on the connection
+     * 
+     * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage)
+     * @return ISQLServerMessageHandler
+     */
+    ISQLServerMessageHandler getServerMessageHandler();
+
+    /**
+     * Set message handler on the connection
+     * 
+     * @param messageHandler
+     *        message handler
+     * 
+     * @see ISQLServerMessageHandler#messageHandler(ISQLServerMessage)
+     * @return ISQLServerMessageHandler
+     */
+    ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler);
+
     /**
      * Returns the current flag for calcBigDecimalPrecision.
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java
new file mode 100644
index 00000000..622b865c
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java
@@ -0,0 +1,99 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc;
+
+import java.sql.SQLException;
+
+
+/**
+ * Provides an interface SQLServerMessage
+ */
+public interface ISQLServerMessage {
+    /**
+     * Returns SQLServerError containing detailed info about SQL Server Message as received from SQL Server.
+     * 
+     * @return SQLServerError
+     */
+    public SQLServerError getSQLServerMessage();
+
+    /**
+     * Returns error message as received from SQL Server
+     * 
+     * @return Error Message
+     */
+    public String getErrorMessage();
+
+    /**
+     * Returns error number as received from SQL Server
+     * 
+     * @return Error Number
+     */
+    public int getErrorNumber();
+
+    /**
+     * Returns error state as received from SQL Server
+     * 
+     * @return Error State
+     */
+    public int getErrorState();
+
+    /**
+     * Returns Severity of error (as int value) as received from SQL Server
+     * 
+     * @return Error Severity
+     */
+    public int getErrorSeverity();
+
+    /**
+     * Returns name of the server where exception occurs as received from SQL Server
+     * 
+     * @return Server Name
+     */
+    public String getServerName();
+
+    /**
+     * Returns name of the stored procedure where exception occurs as received from SQL Server
+     * 
+     * @return Procedure Name
+     */
+    public String getProcedureName();
+
+    /**
+     * Returns line number where the error occurred in Stored Procedure returned by <code>getProcedureName()</code> as
+     * received from SQL Server
+     * 
+     * @return Line Number
+     */
+    public long getLineNumber();
+
+    /**
+     * Creates a SQLServerException or SQLServerWarning from this SQLServerMessage<br>
+     * 
+     * @return
+     *         <ul>
+     *         <li>SQLServerException if it's a SQLServerError object</li>
+     *         <li>SQLServerWarning if it's a SQLServerInfoMessage object</li>
+     *         </ul>
+     */
+    public SQLException toSqlExceptionOrSqlWarning();
+
+    /**
+     * Check if this is a isErrorMessage
+     * 
+     * @return true if it's an instance of SQLServerError
+     */
+    public default boolean isErrorMessage() {
+        return this instanceof SQLServerError;
+    }
+
+    /**
+     * Check if this is a SQLServerInfoMessage
+     * 
+     * @return true if it's an instance of SQLServerInfoMessage
+     */
+    public default boolean isInfoMessage() {
+        return this instanceof SQLServerInfoMessage;
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java
new file mode 100644
index 00000000..2f0b71de
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc;
+
+/**
+ * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server.
+ * Implementing ISQLServerMessageHandler in your own class for handling error messages can provide the following benefits:
+ * <ul>
+ * <li><b>"message feedback"</b><br>
+ * Display Server messages from a long running SQL Statement<br>
+ * Like <code>RAISERROR ('Progress message...', 0, 1) WITH NOWAIT</code><br>
+ * Or Status messages from a running backup...<br>
+ * </li>
+ * <li><b>"Universal" error logging</b><br>
+ * Your error-message handler can contain the logic for handling all error logging.
+ * </li>
+ * <li><b>"Universal" error handling</b><br>
+ * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application.
+ * </li>
+ * <li><b>Remapping of error-message severity</b>, based on application requirements<br>
+ * Your error-message handler can contain logic for recognizing specific error messages, and downgrading or upgrading
+ * their severity based on application considerations rather than the severity rating of the server.
+ * For example, during a cleanup operation that deletes old rows, you might want to downgrade the severity of a
+ * message that a row does not exist. However, you may want to upgrade the severity in other circumstances.
+ * </li>
+ * </ul>
+ * <p>
+ * For example code, see {@link #messageHandler(ISQLServerMessage)}
+ */
+public interface ISQLServerMessageHandler {
+    /**
+     * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server.
+     * Implementing ISQLServerMessageHandler in your own class for handling error messages can provide the following benefits:
+     * <ul>
+     * <li><b>"message feedback"</b><br>
+     * Display Server messages from a long running SQL Statement<br>
+     * Like <code>RAISERROR ('Progress message...', 0, 1) WITH NOWAIT</code><br>
+     * Or Status messages from a running backup...<br>
+     * </li>
+     * <li><b>"Universal" error logging</b><br>
+     * Your error-message handler can contain the logic for handling all error logging.
+     * </li>
+     * <li><b>"Universal" error handling</b><br>
+     * Error-handling logic can be placed in your error-message handler, instead of being repeated throughout your application.
+     * </li>
+     * <li><b>Remapping of error-message severity</b>, based on application requirements<br>
+     * Your error-message handler can contain logic for recognizing specific error messages, and downgrading or upgrading
+     * their severity based on application considerations rather than the severity rating of the server.
+     * For example, during a cleanup operation that deletes old rows, you might want to downgrade the severity of a
+     * message that a row does not exist. However, you may want to upgrade the severity in other circumstances.
+     * </li>
+     * </ul>
+     * 
+     * Example code:
+     * 
+     * <pre>
+     * public ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning) {
+     *     ISQLServerMessage retObj = serverErrorOrWarning;
+     *
+     *     if (serverErrorOrWarning.isErrorMessage()) {
+     *
+     *         // Downgrade: 2601 -- Cannot insert duplicate key row...
+     *         if (2601 == serverErrorOrWarning.getErrorNumber()) {
+     *             retObj = serverErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+     *         }
+     *
+     *         // Discard: 3701 -- Cannot drop the table ...
+     *         if (3701 == serverErrorOrWarning.getErrorNumber()) {
+     *             retObj = null;
+     *         }
+     *     }
+     *
+     *     return retObj;
+     * }
+     * 
+     * </pre>
+     * 
+     * @param serverErrorOrWarning
+     *        server error or warning
+     * @return
+     *         <ul>
+     *         <li><b>unchanged</b> same object as passed in.<br>
+     *         The JDBC driver will work as if no message hander was installed<br>
+     *         Possibly used for logging functionality<br>
+     *         </li>
+     *         <li><b>null</b><br>
+     *         The JDBC driver will <i>discard</i> this message. No SQLException will be thrown
+     *         </li>
+     *         <li><b>SQLServerInfoMessage</b> object<br>
+     *         Create a "SQL warning" from a input database error, and return it.
+     *         This results in the warning being added to the warning-message chain.
+     *         </li>
+     *         <li><b>SQLServerError</b> object<br>
+     *         If the originating message is a SQL warning (SQLServerInfoMessage object), messageHandler can evaluate
+     *         the SQL warning as urgent and create and return a SQL exception (SQLServerError object)
+     *         to be thrown once control is returned to the JDBC Driver.
+     *         </li>
+     *         </ul>
+     */
+    ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning);
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
index 31b87fb2..24458a7f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
@@ -22,13 +22,16 @@ import java.util.concurrent.locks.ReentrantLock;
  * @see <a href="https://aka.ms/msal4j-token-cache">https://aka.ms/msal4j-token-cache</a>
  */
 public class PersistentTokenCacheAccessAspect implements ITokenCacheAccessAspect {
-    private static PersistentTokenCacheAccessAspect instance = new PersistentTokenCacheAccessAspect();
-
+    private static PersistentTokenCacheAccessAspect instance;
     private final Lock lock = new ReentrantLock();
 
-    private PersistentTokenCacheAccessAspect() {}
+    static final long TIME_TO_LIVE = 86400000L; // Token cache time to live (24 hrs).
+    private long expiryTime;
 
     static PersistentTokenCacheAccessAspect getInstance() {
+        if (instance == null) {
+            instance = new PersistentTokenCacheAccessAspect();
+        }
         return instance;
     }
 
@@ -62,6 +65,25 @@ public class PersistentTokenCacheAccessAspect implements ITokenCacheAccessAspect
 
     }
 
+    /**
+     * Get expiry time
+     * 
+     * @return expiry time
+     */
+    public long getExpiryTime() {
+        return this.expiryTime;
+    }
+
+    /**
+     * Set expiry time
+     * 
+     * @param expiryTime
+     *        expiry time
+     */
+    public void setExpiryTime(long expiryTime) {
+        this.expiryTime = expiryTime;
+    }
+
     /**
      * Clears User token cache. This will clear all account info so interactive login will be required on the next
      * request to acquire an access token.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
index 89a433f0..42281688 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
@@ -919,6 +919,22 @@ public class SQLServerColumnEncryptionAzureKeyVaultProvider extends SQLServerCol
         return isValid;
     }
 
+    /**
+     * Sign column master key metadata
+     * 
+     * @param masterKeyPath
+     *        master key path
+     * 
+     * @param allowEnclaveComputations
+     *        flag whether to allow enclave computations
+     * 
+     * @return
+     *         column master key metadata
+     * 
+     * @throws SQLServerException
+     *         when an error occurs
+     * 
+     */
     public byte[] signColumnMasterKeyMetadata(String masterKeyPath,
             boolean allowEnclaveComputations) throws SQLServerException {
         if (!allowEnclaveComputations) {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
index 50359f04..d2e2f28c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
@@ -164,6 +164,22 @@ public class SQLServerColumnEncryptionJavaKeyStoreProvider extends SQLServerColu
         return isValid;
     }
 
+    /**
+     * Sign column master key metadata
+     * 
+     * @param masterKeyPath
+     *        master key path
+     * 
+     * @param allowEnclaveComputations
+     *        flag whether to allow enclave computations
+     * 
+     * @return
+     *         column master key metadata
+     * 
+     * @throws SQLServerException
+     *         when an error occurs
+     * 
+     */
     public byte[] signColumnMasterKeyMetadata(String masterKeyPath,
             boolean allowEnclaveComputations) throws SQLServerException {
         if (!allowEnclaveComputations)
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index b20b6e7f..7a72a047 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -1053,6 +1053,9 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         this.ignoreOffsetOnDateTimeOffsetConversion = ignoreOffsetOnDateTimeOffsetConversion;
     }
 
+    /**
+     * Flag to indicate whether the driver should calculate precision for BigDecimal inputs, as opposed to using the maximum allowed valued for precision (38).
+     */
     private boolean calcBigDecimalPrecision = SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION
             .getDefaultValue();
 
@@ -3207,9 +3210,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
             state = State.OPENED;
 
+            // Socket timeout is bounded by loginTimeout during the login phase.
+            // Reset socket timeout back to the original value.
+            tdsChannel.resetTcpSocketTimeout();
+
             if (connectionlogger.isLoggable(Level.FINER)) {
                 connectionlogger.finer(toString() + " End of connect");
             }
+        } catch (SocketException e) {
+            throw new SQLServerException(e.getMessage(), null);
         } finally {
             // once we exit the connect function, the connection can be only in one of two
             // states, Opened or Closed(if an exception occurred)
@@ -4927,6 +4936,27 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    // Any changes to SQLWarnings should be synchronized.
+    /** Used to add plain SQLWarning messages (if they do not hold extended information, like: ErrorSeverity, ServerName, ProcName etc */
+    void addWarning(SQLWarning sqlWarning) {
+        warningSynchronization.lock();
+        try {
+            if (null == sqlWarnings) {
+                sqlWarnings = sqlWarning;
+            } else {
+                sqlWarnings.setNextWarning(sqlWarning);
+            }
+        } finally {
+            warningSynchronization.unlock();
+        }
+    }
+
+    // Any changes to SQLWarnings should be synchronized.
+    /** Used to add messages that holds extended information, like: ErrorSeverity, ServerName, ProcName etc */
+    void addWarning(ISQLServerMessage sqlServerMessage) {
+        addWarning(new SQLServerWarning(sqlServerMessage.getSQLServerMessage()));
+    }
+
     @Override
     public void clearWarnings() throws SQLServerException {
         warningSynchronization.lock();
@@ -8499,6 +8529,31 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     public String getIPAddressPreference() {
         return activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
     }
+
+    /** Message handler */
+    private transient ISQLServerMessageHandler serverMessageHandler;
+
+    /**
+     * Set current message handler
+     * 
+     * @param messageHandler
+     *        message handler
+     * @return The previously installed message handler (null if none)
+     */
+    @Override
+    public ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler) {
+        ISQLServerMessageHandler installedMessageHandler = this.serverMessageHandler;
+        this.serverMessageHandler = messageHandler;
+        return installedMessageHandler;
+    }
+
+    /**
+     * @return Get Currently installed message handler on the connection
+     */
+    @Override
+    public ISQLServerMessageHandler getServerMessageHandler() {
+        return this.serverMessageHandler;
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index ffda2dd6..5ed4b7b0 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -692,6 +692,16 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
         wrappedConnection.setAccessTokenCallbackClass(accessTokenCallbackClass);
     }
 
+    @Override
+    public ISQLServerMessageHandler getServerMessageHandler() {
+        return wrappedConnection.getServerMessageHandler();
+    }
+    
+    @Override
+    public ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler) {
+    	return wrappedConnection.setServerMessageHandler(messageHandler);
+    }
+
     /**
      * Returns the current value for 'calcBigDecimalPrecision'.
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
index 2dfb40d8..5c23a477 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
@@ -268,7 +268,10 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     // Use LinkedHashMap to force retrieve elements in order they were inserted
     /** getColumns columns */
     private LinkedHashMap<Integer, String> getColumnsDWColumns = null;
+
+    /** getTypes columns */
     private LinkedHashMap<Integer, String> getTypesDWColumns = null;
+
     /** getImportedKeys columns */
     private volatile LinkedHashMap<Integer, String> getImportedKeysDWColumns;
     private static final Lock LOCK = new ReentrantLock();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java
index 3454e5ae..b3e96904 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java
@@ -6,12 +6,15 @@
 package com.microsoft.sqlserver.jdbc;
 
 import java.io.Serializable;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
 
 
 /**
  * SQLServerError represents a TDS error or message event.
  */
-public final class SQLServerError extends StreamPacket implements Serializable {
+public final class SQLServerError extends StreamPacket implements Serializable, ISQLServerMessage {
 
     /**
      * List SQL Server transient errors drivers will retry on from
@@ -221,6 +224,18 @@ public final class SQLServerError extends StreamPacket implements Serializable {
         super(TDS.TDS_ERR);
     }
 
+    SQLServerError(SQLServerError errorMsg) {
+        super(TDS.TDS_ERR);
+        this.errorNumber = errorMsg.errorNumber;
+        this.errorState = errorMsg.errorState;
+        this.errorSeverity = errorMsg.errorSeverity;
+        this.errorMessage = errorMsg.errorMessage;
+        this.serverName = errorMsg.serverName;
+        this.procName = errorMsg.procName;
+        this.lineNumber = errorMsg.lineNumber;
+    }
+
+    @Override
     void setFromTDS(TDSReader tdsReader) throws SQLServerException {
         if (TDS.TDS_ERR != tdsReader.readUnsignedByte())
             assert false;
@@ -237,4 +252,103 @@ public final class SQLServerError extends StreamPacket implements Serializable {
         procName = tdsReader.readUnicodeString(tdsReader.readUnsignedByte());
         lineNumber = tdsReader.readUnsignedInt();
     }
+
+    /**
+     * Holds any "overflow messages", or messages that has been added <b>after</b> the first message.
+     * <p>
+     * This is later on used when creating a SQLServerException.<br>
+     * Where all entries in the errorChain will be added {@link java.sql.SQLException#setNextException(SQLException)}
+     */
+    private List<SQLServerError> errorChain;
+
+    void addError(SQLServerError sqlServerError) {
+        if (errorChain == null) {
+            errorChain = new ArrayList<>();
+        }
+        errorChain.add(sqlServerError);
+    }
+
+    List<SQLServerError> getErrorChain() {
+        return errorChain;
+    }
+
+    @Override
+    public SQLServerError getSQLServerMessage() {
+        return this;
+    }
+
+    /**
+     * Downgrade a Error message into a Info message
+     * <p>
+     * This simply create a SQLServerInfoMessage from this SQLServerError,
+     * without changing the message content.
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerInfoMessage() {
+        return toSQLServerInfoMessage(-1, -1);
+    }
+
+    /**
+     * Downgrade a Error message into a Info message
+     * <p>
+     * This simply create a SQLServerInfoMessage from this SQLServerError,
+     * 
+     * @param newErrorSeverity
+     *        - The new ErrorSeverity
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity) {
+        return toSQLServerInfoMessage(newErrorSeverity, -1);
+    }
+
+    /**
+     * Downgrade a Error message into a Info message
+     * <p>
+     * This simply create a SQLServerInfoMessage from this SQLServerError,
+     * 
+     * @param newErrorSeverity
+     *        - If you want to change the ErrorSeverity (-1: leave unchanged)
+     * @param newErrorNumber
+     *        - If you want to change the ErrorNumber (-1: leave unchanged)
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity, int newErrorNumber) {
+        if (newErrorSeverity != -1) {
+            this.setErrorSeverity(newErrorSeverity);
+        }
+
+        if (newErrorNumber != -1) {
+            this.setErrorNumber(newErrorNumber);
+        }
+
+        return new SQLServerInfoMessage(this);
+    }
+
+    /**
+     * Set a new ErrorSeverity for this Message
+     * 
+     * @param newSeverity
+     *        new severity
+     */
+    public void setErrorSeverity(int newSeverity) {
+        this.errorSeverity = newSeverity;
+    }
+
+    /**
+     * Set a new ErrorNumber for this Message
+     * 
+     * @param newErrorNumber
+     *        new error number
+     */
+    public void setErrorNumber(int newErrorNumber) {
+        this.errorNumber = newErrorNumber;
+    }
+
+    @Override
+    public SQLException toSqlExceptionOrSqlWarning() {
+        return new SQLServerException(this);
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
index 943804dc..77367708 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
@@ -7,6 +7,7 @@ package com.microsoft.sqlserver.jdbc;
 
 import java.sql.SQLFeatureNotSupportedException;
 import java.text.MessageFormat;
+import java.util.List;
 import java.util.UUID;
 import java.util.logging.Level;
 
@@ -178,6 +179,15 @@ public final class SQLServerException extends java.sql.SQLException {
         logException(obj, errText, bStack);
     }
 
+    public SQLServerException(SQLServerError sqlServerError) {
+        super(sqlServerError.getErrorMessage(), 
+            generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), 
+            sqlServerError.getErrorNumber(), 
+            null);
+
+        this.sqlServerError = sqlServerError;
+	}
+
     /**
      * Constructs a new SQLServerException.
      * 
@@ -261,6 +271,21 @@ public final class SQLServerException extends java.sql.SQLException {
                 SQLServerException.checkAndAppendClientConnId(errText, con), state, sqlServerError, bStack);
         theException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE);
 
+        // Add any extra messages to the SQLException error chain
+        List<SQLServerError> errorChain = sqlServerError.getErrorChain();
+        if (errorChain != null) {
+            for (SQLServerError srvError : errorChain) {
+                String state2 = generateStateCode(con, srvError.getErrorNumber(), srvError.getErrorState());
+
+                SQLServerException chainException = new SQLServerException(obj,
+                        SQLServerException.checkAndAppendClientConnId(srvError.getErrorMessage(), con), 
+                        state2, srvError, bStack);
+                chainException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE);
+                
+                theException.setNextException(chainException);
+            }
+        }
+
         // Close the connection if we get a severity 20 or higher error class (nClass is severity of error).
         if ((sqlServerError.getErrorSeverity() >= 20) && (null != con)) {
             con.notifyPooledConnection(theException);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java
new file mode 100644
index 00000000..0b3a6075
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java
@@ -0,0 +1,135 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import java.sql.SQLException;
+
+
+/**
+ * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10)
+ * <p>
+ * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the
+ * SQL Servers extended information, like: ErrorSeverity, ServerName, ProcName etc
+ * <p>
+ * This enables client to print out extra information about the message.<br>
+ * Like: In what procedure was the message produced.
+ * <p>
+ * A SQLServerInfoMessage is produced when reading the TDS Stream and added to the Connection as a SQLServerWarning
+ */
+public final class SQLServerInfoMessage extends StreamPacket implements ISQLServerMessage {
+    SQLServerError msg = new SQLServerError();
+
+    SQLServerInfoMessage() {
+        super(TDS.TDS_MSG);
+    }
+
+    SQLServerInfoMessage(SQLServerError msg) {
+        super(TDS.TDS_MSG);
+        this.msg = msg;
+    }
+
+    @Override
+    void setFromTDS(TDSReader tdsReader) throws SQLServerException {
+        if (TDS.TDS_MSG != tdsReader.readUnsignedByte())
+            assert false;
+        msg.setContentsFromTDS(tdsReader);
+    }
+
+    @Override
+    public SQLServerError getSQLServerMessage() {
+        return msg;
+    }
+
+    @Override
+    public String getErrorMessage() {
+        return msg.getErrorMessage();
+    }
+
+    @Override
+    public int getErrorNumber() {
+        return msg.getErrorNumber();
+    }
+
+    @Override
+    public int getErrorState() {
+        return msg.getErrorState();
+    }
+
+    @Override
+    public int getErrorSeverity() {
+        return msg.getErrorSeverity();
+    }
+
+    @Override
+    public String getServerName() {
+        return msg.getServerName();
+    }
+
+    @Override
+    public String getProcedureName() {
+        return msg.getProcedureName();
+    }
+
+    @Override
+    public long getLineNumber() {
+        return msg.getLineNumber();
+    }
+
+    /**
+     * Upgrade a Info message into a Error message
+     * <p>
+     * This simply create a SQLServerError from this SQLServerInfoMessage,
+     * without changing the message content.
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerError() {
+        return toSQLServerError(-1, -1);
+    }
+
+    /**
+     * Upgrade a Info message into a Error message
+     * <p>
+     * This simply create a SQLServerError from this SQLServerInfoMessage.
+     * 
+     * @param newErrorSeverity
+     *        - The new ErrorSeverity
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerError(int newErrorSeverity) {
+        return toSQLServerError(newErrorSeverity, -1);
+    }
+
+    /**
+     * Upgrade a Info message into a Error message
+     * <p>
+     * This simply create a SQLServerError from this SQLServerInfoMessage.
+     * 
+     * @param newErrorSeverity
+     *        - If you want to change the ErrorSeverity (-1: leave unchanged)
+     * @param newErrorNumber
+     *        - If you want to change the ErrorNumber (-1: leave unchanged)
+     * 
+     * @return ISQLServerMessage
+     */
+    public ISQLServerMessage toSQLServerError(int newErrorSeverity, int newErrorNumber) {
+        if (newErrorSeverity != -1) {
+            this.msg.setErrorSeverity(newErrorSeverity);
+        }
+
+        if (newErrorNumber != -1) {
+            this.msg.setErrorNumber(newErrorNumber);
+        }
+
+        return new SQLServerError(this.msg);
+    }
+
+    @Override
+    public SQLException toSqlExceptionOrSqlWarning() {
+        return new SQLServerWarning(this.msg);
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
index c8f2fb34..1ae69891 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
@@ -14,6 +14,7 @@ import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 
+import java.security.MessageDigest;
 import java.text.MessageFormat;
 
 import java.util.Collections;
@@ -21,6 +22,7 @@ import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -61,6 +63,8 @@ class SQLServerMSAL4JUtils {
     static final String SLASH_DEFAULT = "/.default";
     static final String ACCESS_TOKEN_EXPIRE = "access token expires: ";
 
+    private static final TokenCacheMap TOKEN_CACHE_MAP = new TokenCacheMap();
+
     private final static String LOGCONTEXT = "MSAL version "
             + com.microsoft.aad.msal4j.PublicClientApplication.class.getPackage().getImplementationVersion() + ": ";
 
@@ -84,10 +88,17 @@ class SQLServerMSAL4JUtils {
         lock.lock();
 
         try {
+            String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, user, password});
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+
+            if (null == persistentTokenCacheAccessAspect) {
+                persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+            }
+
             final PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
-                    .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
-                    .authority(fedAuthInfo.stsurl).build();
+                    .setTokenCacheAccessAspect(persistentTokenCacheAccessAspect).authority(fedAuthInfo.stsurl).build();
 
             final CompletableFuture<IAuthenticationResult> future = pca.acquireToken(UserNamePasswordParameters
                     .builder(Collections.singleton(fedAuthInfo.spn + SLASH_DEFAULT), user, password.toCharArray())
@@ -132,11 +143,19 @@ class SQLServerMSAL4JUtils {
         lock.lock();
 
         try {
+            String hashedSecret = getHashedSecret(
+                    new String[] {fedAuthInfo.stsurl, aadPrincipalID, aadPrincipalSecret});
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+
+            if (null == persistentTokenCacheAccessAspect) {
+                persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+            }
+
             IClientCredential credential = ClientCredentialFactory.createFromSecret(aadPrincipalSecret);
             ConfidentialClientApplication clientApplication = ConfidentialClientApplication
                     .builder(aadPrincipalID, credential).executorService(executorService)
-                    .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
-                    .authority(fedAuthInfo.stsurl).build();
+                    .setTokenCacheAccessAspect(persistentTokenCacheAccessAspect).authority(fedAuthInfo.stsurl).build();
 
             final CompletableFuture<IAuthenticationResult> future = clientApplication
                     .acquireToken(ClientCredentialParameters.builder(scopes).build());
@@ -181,6 +200,15 @@ class SQLServerMSAL4JUtils {
         lock.lock();
 
         try {
+            String hashedSecret = getHashedSecret(new String[] {fedAuthInfo.stsurl, aadPrincipalID, certFile,
+                    certPassword, certKey, certKeyPassword});
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = TOKEN_CACHE_MAP.getEntry(hashedSecret);
+
+            if (null == persistentTokenCacheAccessAspect) {
+                persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                TOKEN_CACHE_MAP.addEntry(hashedSecret, persistentTokenCacheAccessAspect);
+            }
+
             ConfidentialClientApplication clientApplication = null;
 
             // check if cert is PKCS12 first
@@ -202,8 +230,7 @@ class SQLServerMSAL4JUtils {
 
                 IClientCredential credential = ClientCredentialFactory.createFromCertificate(is, certPassword);
                 clientApplication = ConfidentialClientApplication.builder(aadPrincipalID, credential)
-                        .executorService(executorService)
-                        .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
+                        .executorService(executorService).setTokenCacheAccessAspect(persistentTokenCacheAccessAspect)
                         .authority(fedAuthInfo.stsurl).build();
             } catch (FileNotFoundException e) {
                 // re-throw if file not there no point to try another format
@@ -232,8 +259,7 @@ class SQLServerMSAL4JUtils {
 
                 IClientCredential credential = ClientCredentialFactory.createFromCertificate(privateKey, cert);
                 clientApplication = ConfidentialClientApplication.builder(aadPrincipalID, credential)
-                        .executorService(executorService)
-                        .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
+                        .executorService(executorService).setTokenCacheAccessAspect(persistentTokenCacheAccessAspect)
                         .authority(fedAuthInfo.stsurl).build();
             }
 
@@ -449,4 +475,45 @@ class SQLServerMSAL4JUtils {
             return new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException);
         }
     }
+
+    private static String getHashedSecret(String[] secrets) throws SQLServerException {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+            for (String secret : secrets) {
+                if (null != secret) {
+                    md.update(secret.getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
+                }
+            }
+            return new String(md.digest());
+        } catch (NoSuchAlgorithmException e) {
+            throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e);
+        }
+    }
+
+    private static class TokenCacheMap {
+        private ConcurrentHashMap<String, PersistentTokenCacheAccessAspect> tokenCacheMap = new ConcurrentHashMap<>();
+
+        PersistentTokenCacheAccessAspect getEntry(String key) {
+            PersistentTokenCacheAccessAspect persistentTokenCacheAccessAspect = tokenCacheMap.get(key);
+
+            if (null != persistentTokenCacheAccessAspect) {
+                if (System.currentTimeMillis() > persistentTokenCacheAccessAspect.getExpiryTime()) {
+                    tokenCacheMap.remove(key);
+
+                    persistentTokenCacheAccessAspect = new PersistentTokenCacheAccessAspect();
+                    persistentTokenCacheAccessAspect
+                            .setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE);
+
+                    tokenCacheMap.put(key, persistentTokenCacheAccessAspect);
+                }
+            }
+
+            return persistentTokenCacheAccessAspect;
+        }
+
+        void addEntry(String key, PersistentTokenCacheAccessAspect value) {
+            value.setExpiryTime(System.currentTimeMillis() + PersistentTokenCacheAccessAspect.TIME_TO_LIVE);
+            tokenCacheMap.put(key, value);
+        }
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
index 7789b372..4274e691 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
@@ -71,10 +71,15 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
     /** Processed SQL statement text, may not be same as what user initially passed. */
     final String userSQL;
 
+    // flag whether is exec escape syntax
     private boolean isExecEscapeSyntax;
 
+    // flag whether is call escape syntax
     private boolean isCallEscapeSyntax;
 
+    // flag whether is four part syntax
+    private boolean isFourPartSyntax;
+
     /** Parameter positions in processed SQL statement text. */
     final int[] userSQLParamPositions;
 
@@ -144,6 +149,11 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
      */
     private static final Pattern execEscapePattern = Pattern.compile("^\\s*(?i)(?:exec|execute)\\b");
 
+    /**
+     * Regex for four part syntax
+     */
+    private static final Pattern fourPartSyntaxPattern = Pattern.compile("(.+)\\.(.+)\\.(.+)\\.(.+)");
+
     /** Returns the prepared statement SQL */
     @Override
     public String toString() {
@@ -271,6 +281,7 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
         userSQL = parsedSQL.processedSQL;
         isExecEscapeSyntax = isExecEscapeSyntax(sql);
         isCallEscapeSyntax = isCallEscapeSyntax(sql);
+        isFourPartSyntax = isFourPartSyntax(sql);
         userSQLParamPositions = parsedSQL.parameterPositions;
         initParams(userSQLParamPositions.length);
         useBulkCopyForBatchInsert = conn.getUseBulkCopyForBatchInsert();
@@ -1234,10 +1245,12 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
         // 2. There must be parameters
         // 3. Parameters must not be a TVP type
         // 4. Compliant CALL escape syntax
-        // If isExecEscapeSyntax is true, EXEC escape syntax is used then use prior behaviour to
-        // execute the procedure
+        // If isExecEscapeSyntax is true, EXEC escape syntax is used then use prior behaviour of
+        // wrapping call to execute the procedure
+        // If isFourPartSyntax is true, sproc is being executed against linked server, then
+        // use prior behaviour of wrapping call to execute procedure
         return (null != procedureName && paramCount != 0 && !isTVPType(params) && isCallEscapeSyntax
-                && !isExecEscapeSyntax);
+                && !isExecEscapeSyntax && !isFourPartSyntax);
     }
 
     /**
@@ -1265,6 +1278,10 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
         return callEscapePattern.matcher(sql).find();
     }
 
+    private boolean isFourPartSyntax(String sql) {
+        return fourPartSyntaxPattern.matcher(sql).find();
+    }
+
     /**
      * Executes sp_prepare to prepare a parameterized statement and sets the prepared statement handle
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
index 866b9ea1..cbfe102c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
@@ -1620,7 +1620,6 @@ public class SQLServerStatement implements ISQLServerStatement {
                         }
                     }
                 }
-
                 // If the current command (whatever it was) produced an error then stop parsing and propagate it up.
                 // In this case, the command is likely to be a RAISERROR, but it could be anything.
                 if (doneToken.isError())
@@ -1690,8 +1689,8 @@ public class SQLServerStatement implements ISQLServerStatement {
 
             @Override
             boolean onInfo(TDSReader tdsReader) throws SQLServerException {
-                StreamInfo infoToken = new StreamInfo();
-                infoToken.setFromTDS(tdsReader);
+                SQLServerInfoMessage infoMessage = new SQLServerInfoMessage();
+                infoMessage.setFromTDS(tdsReader);
 
                 // Under some circumstances the server cannot produce the cursored result set
                 // that we requested, but produces a client-side (default) result set instead.
@@ -1705,13 +1704,40 @@ public class SQLServerStatement implements ISQLServerStatement {
                 // ErrorCause: Server cursor is not supported on the specified SQL, falling back to default result set
                 // ErrorCorrectiveAction: None required
                 //
-                if (16954 == infoToken.msg.getErrorNumber())
+                if (16954 == infoMessage.msg.getErrorNumber())
                     executedSqlDirectly = true;
 
-                SQLWarning warning = new SQLWarning(
-                        infoToken.msg.getErrorMessage(), SQLServerException.generateStateCode(connection,
-                                infoToken.msg.getErrorNumber(), infoToken.msg.getErrorState()),
-                        infoToken.msg.getErrorNumber());
+
+                // Call the message handler to see what that think of the message
+                // - discard
+                // - upgrade to Error
+                // - or simply pass on
+                ISQLServerMessageHandler msgHandler = ((ISQLServerConnection)getConnection()).getServerMessageHandler();
+                if (msgHandler != null) {
+
+                    // Let the message handler decide if the error should be unchanged, up/down-graded or ignored
+                    ISQLServerMessage srvMessage = msgHandler.messageHandler(infoMessage);
+                
+                    // Ignored
+                    if (srvMessage == null) {
+                    	return true;
+                    }
+                
+                    // The message handler changed it to an "Error Message"
+                    if (srvMessage.isErrorMessage()) {
+                        // Set/Add the error message to the "super"
+                        addDatabaseError( (SQLServerError)srvMessage );
+                        return true;
+                    }
+                    
+                    // Still a "info message", just set infoMessage and the code in the below section will create the Warnings
+                    if (srvMessage.isInfoMessage()) {
+                        infoMessage = (SQLServerInfoMessage)srvMessage;
+                    }
+                }
+
+                // Create the SQLWarning and add them to the Warning chain
+                SQLWarning warning = new SQLServerWarning(infoMessage.msg);
 
                 if (sqlWarnings == null) {
                     sqlWarnings = new Vector<>();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java
new file mode 100644
index 00000000..81d588bd
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java
@@ -0,0 +1,47 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc;
+
+import java.sql.SQLWarning;
+
+/**
+ * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10)
+ * <p>
+ * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the 
+ * SQL Servers extended information, like: ErrorSeverity, ServerName, ProcName etc
+ * <p>
+ * This enables client to print out extra information about the message.<br>
+ * Like: In what procedure was the message produced.
+ */
+public class SQLServerWarning
+extends SQLWarning
+{
+    private static final long serialVersionUID = -5212432397705929142L;
+
+    /** SQL server error */
+    private SQLServerError sqlServerError;
+    
+    /*
+     * Create a SQLWarning from an SQLServerError object
+     */
+    public SQLServerWarning(SQLServerError sqlServerError) {
+        super(sqlServerError.getErrorMessage(), 
+            SQLServerException.generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), 
+            sqlServerError.getErrorNumber(), 
+            null);
+
+        this.sqlServerError = sqlServerError;
+    }
+
+    /**
+     * Returns SQLServerError object containing detailed info about exception as received from SQL Server. This API
+     * returns null if no server error has occurred.
+     * 
+     * @return SQLServerError
+     */
+    public SQLServerError getSQLServerError() {
+        return sqlServerError;
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
index 6d82bf83..eb96becb 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
@@ -5,7 +5,6 @@
 
 package com.microsoft.sqlserver.jdbc;
 
-import java.net.SocketException;
 import java.sql.CallableStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -25,7 +24,6 @@ import javax.transaction.xa.XAException;
 import javax.transaction.xa.XAResource;
 import javax.transaction.xa.Xid;
 
-import com.microsoft.sqlserver.jdbc.SQLServerError.TransientError;
 
 
 /**
@@ -754,27 +752,13 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
                     }
                 }
             }
-        } catch (SQLTimeoutException ex) {
+        } catch (SQLTimeoutException | SQLServerException ex) {
             if (xaLogger.isLoggable(Level.FINER))
                 xaLogger.finer(toString() + " exception:" + ex);
             XAException e = new XAException(ex.toString());
             e.errorCode = XAException.XAER_RMFAIL;
             throw e;
 
-        } catch (SQLServerException ex) {
-            if (xaLogger.isLoggable(Level.FINER))
-                xaLogger.finer(toString() + " exception:" + ex);
-
-            if (ex.getMessage().equals(SQLServerException.getErrString("R_noServerResponse"))
-                    || TransientError.isTransientError(ex.getSQLServerError()) || isResourceManagerFailure(ex)) {
-                XAException e = new XAException(ex.toString());
-                e.errorCode = XAException.XAER_RMFAIL;
-                throw e;
-            }
-
-            XAException e = new XAException(ex.toString());
-            e.errorCode = XAException.XAER_RMERR;
-            throw e;
         }
 
         if (xaLogger.isLoggable(Level.FINER))
@@ -945,68 +929,4 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
         return baseResourceID.incrementAndGet();
     }
 
-    private enum ResourceManagerFailure {
-        CONN_RESET("Connection reset"),
-        CONN_RESET_BY_PEER("Connection reset by peer"),
-        CONN_TIMEOUT("Connection timed out"),
-        CONN_RESILIENCY_CLIENT_UNRECOVERABLE(SQLServerException.getErrString("R_crClientUnrecoverable"));
-
-        private final String errString;
-
-        ResourceManagerFailure(String errString) {
-            this.errString = errString;
-        }
-
-        @Override
-        public String toString() {
-            return errString;
-        }
-
-        static ResourceManagerFailure fromString(String errString) {
-            for (ResourceManagerFailure resourceManagerFailure : ResourceManagerFailure.values()) {
-                if (errString.equalsIgnoreCase(resourceManagerFailure.toString())) {
-                    return resourceManagerFailure;
-                }
-            }
-            return null;
-        }
-    }
-
-    /**
-     * Check if the root exception of the throwable should be a XAER_RMFAIL exception
-     *
-     * @param throwable
-     *        The exception to check if the root cause should be a XAER_RMFAIL
-     *
-     * @return True if XAER_RMFAIL, otherwise false
-     */
-    private boolean isResourceManagerFailure(Throwable throwable) {
-        Throwable root = Util.getRootCause(throwable);
-
-        if (null == root) {
-            return false;
-        }
-
-        if (xaLogger.isLoggable(Level.FINE)) {
-            xaLogger.fine(toString() + " Resource manager failure root exception: " + root);
-        }
-
-        ResourceManagerFailure err = ResourceManagerFailure.fromString(root.getMessage());
-
-        if (null == err) {
-            return false;
-        }
-
-        // Add as needed here for future XAER_RMFAIL exceptions
-        switch (err) {
-            case CONN_RESET:
-            case CONN_RESET_BY_PEER:
-            case CONN_TIMEOUT:
-            case CONN_RESILIENCY_CLIENT_UNRECOVERABLE:
-                return true;
-            default:
-                return false;
-        }
-    }
-
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java b/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java
deleted file mode 100644
index 5b4cb571..00000000
--- a/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
- * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
- */
-
-package com.microsoft.sqlserver.jdbc;
-
-final class StreamInfo extends StreamPacket {
-    final SQLServerError msg = new SQLServerError();
-
-    StreamInfo() {
-        super(TDS.TDS_MSG);
-    }
-
-    void setFromTDS(TDSReader tdsReader) throws SQLServerException {
-        if (TDS.TDS_MSG != tdsReader.readUnsignedByte())
-            assert false;
-        msg.setContentsFromTDS(tdsReader);
-    }
-}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
index 70b9c9aa..61bda22b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
@@ -13,8 +13,6 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.text.DecimalFormat;
 import java.text.MessageFormat;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.Locale;
 import java.util.Properties;
@@ -1046,23 +1044,6 @@ final class Util {
         }
         return chars;
     }
-
-    /**
-     * @param throwable
-     *        The exception to find root cause for.
-     *
-     * @return The root cause of the exception, otherwise null if null throwable input
-     */
-    static Throwable getRootCause(Throwable throwable) {
-        final List<Throwable> list = new ArrayList<>();
-
-        while (throwable != null && !list.contains(throwable)) {
-            list.add(throwable);
-            throwable = throwable.getCause();
-        }
-
-        return list.isEmpty() ? null : list.get(list.size() - 1);
-    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
index c18d61a0..90eebb8d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
@@ -197,6 +197,14 @@ class TDSTokenHandler {
         return databaseError;
     }
 
+    public void addDatabaseError(SQLServerError databaseError) {
+        if (this.databaseError == null) {
+            this.databaseError = databaseError;
+        } else {
+            this.databaseError.addError(databaseError);
+        }
+    }
+
     TDSTokenHandler(String logContext) {
         this.logContext = logContext;
     }
@@ -258,13 +266,29 @@ class TDSTokenHandler {
     }
 
     boolean onError(TDSReader tdsReader) throws SQLServerException {
-        if (null == databaseError) {
-            databaseError = new SQLServerError();
-            databaseError.setFromTDS(tdsReader);
-        } else {
-            (new SQLServerError()).setFromTDS(tdsReader);
+        SQLServerError tmpDatabaseError = new SQLServerError();
+        tmpDatabaseError.setFromTDS(tdsReader);
+
+        ISQLServerMessageHandler msgHandler = tdsReader.getConnection().getServerMessageHandler();
+        if (msgHandler != null) {
+            // Let the message handler decide if the error should be unchanged/down-graded or ignored
+            ISQLServerMessage srvMessage = msgHandler.messageHandler(tmpDatabaseError);
+
+            // Ignored
+            if (srvMessage == null) {
+                return true;
+            }
+
+            // Down-graded to a SQLWarning
+            if (srvMessage.isInfoMessage()) {
+                tdsReader.getConnection().addWarning(srvMessage);
+                return true;
+            }
         }
 
+        // set/add the database error
+        addDatabaseError(tmpDatabaseError);
+
         return true;
     }
 


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
index 0e5f26a9..aa4252f9 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
@@ -63,6 +63,11 @@ public final class TestResource extends ListResourceBundle {
             {"R_ConnectionURLNull", "The connection URL is null."},
             {"R_connectionIsNotClosed", "The connection is not closed."},
             {"R_invalidExceptionMessage", "Invalid exception message"},
+            {"R_invalidClientSecret", "AADSTS7000215: Invalid client secret provided"},
+            {"R_invalidCertFields",
+                    "Error reading certificate, please verify the location of the certificate.signed fields invalid"},
+            {"R_invalidAADAuth",
+                    "Failed to authenticate the user {0} in Active Directory (Authentication={1})"},
             {"R_failedValidate", "failed to validate values in $0} "}, {"R_tableNotDropped", "table not dropped. "},
             {"R_connectionReset", "Connection reset"}, {"R_unknownException", "Unknown exception"},
             {"R_deadConnection", "Dead connection should be invalid"},
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
index 6570abf7..51203939 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java
@@ -1150,6 +1150,66 @@ public class CallableStatementTest extends AbstractTest {
         }
     }
 
+    @Test
+    @Tag(Constants.reqExternalSetup)
+    @Tag(Constants.xAzureSQLDB)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.xAzureSQLMI)
+    public void testFourPartSyntaxCallEscapeSyntax() throws SQLException {
+        String table = "serverList";
+
+        try (Statement stmt = connection.createStatement()) {
+            stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table);
+            stmt.execute("CREATE TABLE " + table + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)");
+            stmt.execute("INSERT " + table + " EXEC sp_helpserver");
+
+            ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'");
+            rs.next();
+
+            if (rs.getInt(1) == 1) {
+                stmt.execute("EXEC sp_dropserver @server='" + linkedServer + "';");
+            }
+
+            stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';");
+            stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @rmtuser=N'" + remoteUser + "', @rmtpassword=N'" + remotePassword + "'");
+            stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;");
+            stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;");
+        }
+
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setServerName(linkedServer);
+        ds.setUser(remoteUser);
+        ds.setPassword(remotePassword);
+        ds.setEncrypt(false);
+        ds.setTrustServerCertificate(true);
+
+        try (Connection linkedServerConnection = ds.getConnection(); Statement stmt = linkedServerConnection.createStatement()) {
+            stmt.execute("create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;");
+        }
+
+        try (CallableStatement cstmt = connection.prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) {
+            int sum = 11;
+            int param0 = 1;
+            int param1 = 10;
+            cstmt.setInt(1, param0);
+            cstmt.setInt(2, param1);
+            cstmt.registerOutParameter(3, Types.INTEGER);
+            cstmt.execute();
+            assertEquals(sum, cstmt.getInt(3));
+        }
+
+        try (CallableStatement cstmt = connection.prepareCall("exec [" + linkedServer + "].master.dbo.TestAdd ?,?,?")) {
+            int sum = 11;
+            int param0 = 1;
+            int param1 = 10;
+            cstmt.setInt(1, param0);
+            cstmt.setInt(2, param1);
+            cstmt.registerOutParameter(3, Types.INTEGER);
+            cstmt.execute();
+            assertEquals(sum, cstmt.getInt(3));
+        }
+    }
+
     /**
      * Cleanup after test
      *
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
index d758950f..926b6a76 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -521,6 +521,10 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         verifiedMethodNames.add("setMsiTokenCacheTtl");
         verifiedMethodNames.add("getAccessTokenCallbackClass");
         verifiedMethodNames.add("setAccessTokenCallbackClass");
+        verifiedMethodNames.add("getServerMessageHandler");
+        verifiedMethodNames.add("setServerMessageHandler");
+        verifiedMethodNames.add("getCalcBigDecimalScale");
+        verifiedMethodNames.add("setCalcBigDecimalScale");
         verifiedMethodNames.add("getUseFlexibleCallableStatements");
         verifiedMethodNames.add("setUseFlexibleCallableStatements");
         verifiedMethodNames.add("getCalcBigDecimalPrecision");
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
index 68fbfc8d..b9cdd4cd 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java
@@ -313,6 +313,18 @@ public class TimeoutTest extends AbstractTest {
                 "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval));
     }
 
+    @Test
+    public void testSocketTimeoutBoundedByLoginTimeoutReset() throws Exception {
+        try (Connection con = PrepUtil.getConnection(connectionString + ";socketTimeout=90000;loginTimeout=10;");
+                Statement stmt = con.createStatement()) {
+            // Login timeout (10s) is less than the 15s sec WAITFOR DELAY. Upon a login attempt, socketTimeout should be bounded
+            // by loginTimeout. After a successful login, when executing a query, socketTimeout should be reset to the
+            // original 90000ms timeout. The statement below should successfully execute as socketTimeout should not be bounded
+            // by loginTimeout, otherwise the test fails with a socket read timeout error.
+            stmt.execute("WAITFOR DELAY '00:00:15';");
+        }
+    }
+
     // Test for detecting Azure server for connection retries
     @Test
     public void testAzureEndpointRetry() {
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java
new file mode 100644
index 00000000..4c05d2fb
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java
@@ -0,0 +1,74 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc.exception;
+
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@RunWith(JUnitPlatform.class)
+public class ChainedExceptionTest extends AbstractTest {
+
+    @BeforeAll
+    public static void setupTests() throws Exception {
+        setConnection();
+    }
+
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testTwoExceptions() throws Exception {
+        try (Connection conn = getConnection()) {
+
+            // The below should yield the following Server Messages:
+            // 1 : Msg 5074, Level 16, State 1: The object 'DF__#chained_exc__c1__AB25243A' is dependent on column 'c1'.
+            // 1 : Msg 4922, Level 16, State 9: ALTER TABLE ALTER COLUMN c1 failed because one or more objects access this column.
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("CREATE TABLE #chained_exception_test_x1(c1 INT DEFAULT(0))");
+                stmnt.executeUpdate("ALTER  TABLE #chained_exception_test_x1 ALTER COLUMN c1 VARCHAR(10)");
+                stmnt.executeUpdate("DROP   TABLE IF EXISTS #chained_exception_test_x1");
+            }
+
+            fail(TestResource.getResource("R_expectedFailPassed"));
+
+        } catch (SQLException ex) {
+
+            // Check the SQLException and the chain
+            int exCount = 0;
+            int firstMsgNum = ex.getErrorCode();
+            int lastMsgNum = -1;
+
+            while (ex != null) {
+                exCount++;
+
+                lastMsgNum = ex.getErrorCode();
+
+                ex = ex.getNextException();
+            }
+
+            // Exception Count should be: 2
+            assertEquals(2, exCount, "Number of SQLExceptions in the SQLException chain");
+
+            // Check first Msg: 5074
+            assertEquals(5074, firstMsgNum, "First SQL Server Message");
+
+            // Check last Msg: 4922
+            assertEquals(4922, lastMsgNum, "Last SQL Server Message");
+        }
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
index 4bae199d..e68fcacd 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
@@ -18,6 +18,7 @@ import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.text.MessageFormat;
 import java.util.Collections;
 import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
@@ -323,6 +324,66 @@ public class FedauthTest extends FedauthCommon {
         }
     }
 
+    @Test
+    public void testAADServicePrincipalAuthFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidSecret() throws Exception {
+        String url = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication="
+                + SqlAuthentication.ActiveDirectoryServicePrincipal + ";Username=" + applicationClientID + ";Password="
+                + applicationKey;
+
+        String invalidSecretUrl = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication="
+                + SqlAuthentication.ActiveDirectoryServicePrincipal + ";Username=" + applicationClientID + ";Password="
+                + "invalidSecret";
+
+        // Should succeed on valid secret
+        try (Connection connection = DriverManager.getConnection(url)) {}
+
+        // Should fail on invalid secret
+        try (Connection connection = DriverManager.getConnection(invalidSecretUrl)) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().contains(TestResource.getResource("R_invalidClientSecret")),
+                    "Expected R_invalidClientSecret error.");
+        }
+    }
+
+    @Test
+    public void testActiveDirectoryPasswordFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidPassword() throws Exception {
+
+        // Should succeed on valid password
+        try (Connection conn = DriverManager.getConnection(adPasswordConnectionStr)) {}
+
+        // Should fail on invalid password
+        try (Connection conn = DriverManager.getConnection(adPasswordConnectionStr + ";password=invalidPassword;")) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(TestResource.getResource("R_invalidAADAuth"));
+            Object[] msgArgs = {azureUserName, "ActiveDirectoryPassword"};
+            assertTrue(e.getMessage().contains(form.format(msgArgs)), "Expected R_invalidAADAuth error.");
+        }
+    }
+
+    @Test
+    public void testAADServicePrincipalCertAuthFailureOnSubsequentConnectionsWithInvalidatedTokenCacheWithInvalidPassword() throws Exception {
+        // Should succeed on valid cert field values
+        String url = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication="
+                + SqlAuthentication.ActiveDirectoryServicePrincipalCertificate + ";Username=" + applicationClientID
+                + ";password=" + certificatePassword + ";clientCertificate=" + clientCertificate;
+
+        // Should fail on invalid cert field values
+        String invalidPasswordUrl = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase
+                + ";authentication=" + SqlAuthentication.ActiveDirectoryServicePrincipalCertificate + ";Username="
+                + applicationClientID + ";password=invalidPassword;clientCertificate=" + clientCertificate;
+
+        try (Connection conn = DriverManager.getConnection(url)) {}
+
+        try (Connection conn = DriverManager.getConnection(invalidPasswordUrl)) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().contains(TestResource.getResource("R_invalidCertFields")),
+                    "Expected R_invalidCertFields error.");
+        }
+    }
+
     /**
      * Test invalid connection property combinations when using AAD Service Principal Authentication.
      */
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java
new file mode 100644
index 00000000..b9d52a68
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java
@@ -0,0 +1,474 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.jdbc.messageHandler;
+
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import org.junit.jupiter.api.Tag;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import com.microsoft.sqlserver.jdbc.ISQLServerMessage;
+import com.microsoft.sqlserver.jdbc.ISQLServerMessageHandler;
+import com.microsoft.sqlserver.jdbc.SQLServerConnection;
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+@RunWith(JUnitPlatform.class)
+public class MessageHandlerTest extends AbstractTest {
+
+    @BeforeAll
+    public static void setupTests() throws Exception {
+        setConnection();
+    }
+
+    /**
+     * Helper method to count number of SQLWarnings in a chain
+     * 
+     * @param str
+     *        - Debug String, so we can evaluate from where we called it...
+     * @param sqlw
+     *        - The SQL Warning chain. (can be null)
+     * @return A count of warnings
+     */
+    private static int getWarningCount(String str, SQLWarning sqlw) {
+        int count = 0;
+        while (sqlw != null) {
+            count++;
+            // System.out.println("DEBUG: getWarningCount(): [" + str + "] SQLWarning: Error=" + sqlw.getErrorCode() + ", Severity=" + ((SQLServerWarning)sqlw).getSQLServerError().getErrorSeverity() + ", Text=|" + sqlw.getMessage() + "|.");
+            sqlw = sqlw.getNextWarning();
+        }
+        return count;
+    }
+
+    /**
+     * Test message handler with normal Statement
+     * <ul>
+     * <li>Insert duplicate row -- Mapped to Info Message</li>
+     * <li>Drop table that do not exist -- Mapped to ignore</li>
+     * </ul>
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithStatement() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                int numOfDowngrades = 0;
+                int numOfDiscards = 0;
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+                    ISQLServerMessage retObj = srvErrorOrWarning;
+
+                    if (srvErrorOrWarning.isErrorMessage()) {
+
+                        // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1)
+                        if (2601 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+                            numOfDowngrades++;
+                        }
+
+                        // Discard: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission.
+                        if (3701 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    if (srvErrorOrWarning.isInfoMessage()) {
+
+                        // Discard: 3621 -- The statement has been terminated.
+                        if (3621 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    return retObj;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            try (Statement stmnt = conn.createStatement()) {
+
+                stmnt.executeUpdate("CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255))");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Statement.");
+
+                stmnt.executeUpdate("CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id)");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Statement.");
+
+                stmnt.executeUpdate("INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1')");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Statement.");
+
+                stmnt.executeUpdate(
+                        "INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1 - again - msg handler downgrades it')");
+                assertNotNull(conn.getWarnings(),
+                        "Expecting at least ONE SQLWarnings from 'second insert', which is a duplicate row, at Connection.");
+                assertNull(stmnt.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second insert', which is a duplicate row, at Statement.");
+                conn.clearWarnings(); // Clear Warnings at Connection level
+
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement.");
+
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table"); // This should be IGNORED by the message handler
+                assertNull(conn.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second drop table, since it should be IGNORED by the message handler', at Connection.");
+                assertNull(stmnt.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second drop table, since it should be IGNORED by the message handler', at Statement.");
+
+                // numOfCalls to the message handler should be: 3
+                assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+                // numOfDowngrades in the message handler should be: 1
+                assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler.");
+
+                // numOfDiscards in the message handler should be: 2
+                assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler.");
+            }
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+
+    /**
+     * Test message handler with PreparedStatement
+     * <ul>
+     * <li>Insert duplicate row -- Mapped to Info Message</li>
+     * <li>Drop table that do not exist -- Mapped to ignore</li>
+     * </ul>
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithPreparedStatement() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                int numOfDowngrades = 0;
+                int numOfDiscards = 0;
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+                    ISQLServerMessage retObj = srvErrorOrWarning;
+
+                    if (srvErrorOrWarning.isErrorMessage()) {
+
+                        // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1)
+                        if (2601 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+                            numOfDowngrades++;
+                        }
+
+                        // Discard: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission.
+                        if (3701 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    if (srvErrorOrWarning.isInfoMessage()) {
+
+                        // Discard: 3621 -- The statement has been terminated.
+                        if (3621 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    return retObj;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255))");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Statement.");
+            }
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id)");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Statement.");
+            }
+
+            try (PreparedStatement stmnt = conn.prepareStatement("INSERT INTO #msghandler_tmp_table VALUES(?, ?)")) {
+                stmnt.setInt(1, 1);
+                stmnt.setString(2, "row 1");
+                stmnt.executeUpdate();
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Statement.");
+            }
+
+            try (PreparedStatement stmnt = conn.prepareStatement("INSERT INTO #msghandler_tmp_table VALUES(?, ?)")) {
+                stmnt.setInt(1, 1);
+                stmnt.setString(2, "row 1 - again - msg handler downgrades it");
+                stmnt.executeUpdate();
+                assertNotNull(conn.getWarnings(),
+                        "Expecting at least ONE SQLWarnings from 'second insert', which is a duplicate row, at Connection.");
+                assertNull(stmnt.getWarnings(),
+                        "Expecting NO SQLWarnings from 'second insert', which is a duplicate row, at Statement.");
+                conn.clearWarnings(); // Clear Warnings at Connection level
+            }
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement.");
+            }
+
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table");
+                assertNull(conn.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection.");
+                assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement.");
+            }
+
+            // numOfCalls to the message handler should be: 3
+            assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+            // numOfDowngrades in the message handler should be: 1
+            assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler.");
+
+            // numOfDiscards in the message handler should be: 2
+            assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler.");
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+
+    /**
+     * Test message handler with CallableStatement
+     * <ul>
+     * <li>Insert duplicate row -- Mapped to Info Message</li>
+     * <li>Drop table that do not exist -- Mapped to ignore</li>
+     * </ul>
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithCallableStatement() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                int numOfDowngrades = 0;
+                int numOfDiscards = 0;
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+                    ISQLServerMessage retObj = srvErrorOrWarning;
+
+                    if (srvErrorOrWarning.isErrorMessage()) {
+
+                        // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1)
+                        if (2601 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+                            numOfDowngrades++;
+                        }
+
+                        // Downgrade: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission.
+                        if (3701 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    if (srvErrorOrWarning.isInfoMessage()) {
+
+                        // Discard: 3621 -- The statement has been terminated.
+                        if (3621 == srvErrorOrWarning.getErrorNumber()) {
+                            retObj = null;
+                            numOfDiscards++;
+                        }
+                    }
+
+                    return retObj;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            // SQL to create procedure
+            String sqlCreateProc = "" + "CREATE PROCEDURE #msghandler_tmp_proc( \n" + "    @out_row_count INT OUTPUT \n"
+                    + ") \n" + "AS \n" + "BEGIN \n" + "    -- Create a dummy table, with index \n"
+                    + "    CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255)) \n"
+                    + "    CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id) \n" + "     \n"
+                    + "    -- Insert records 1 \n" + "    INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1') \n"
+                    + "     \n"
+                    + "    -- Insert records 1 -- Again, which will FAIL, but the message handler will downgrade it into a INFO Message \n"
+                    + "    INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1 - again - msg handler downgrades it') \n"
+                    + "     \n" + "    -- Count records \n" + "    SELECT @out_row_count = count(*) \n"
+                    + "    FROM #msghandler_tmp_table \n" + "     \n" + "    -- Drop the table \n"
+                    + "    DROP TABLE #msghandler_tmp_table \n" + "     \n"
+                    + "    -- Drop the table agin... The message handler will DISCARD the error\n"
+                    + "    DROP TABLE #msghandler_tmp_table \n" + "     \n" + "    RETURN 1 \n" + "END \n";
+
+            // Create the proc
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate(sqlCreateProc);
+                assertEquals(0, getWarningCount("Conn", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Connection.");
+                assertEquals(0, getWarningCount("Stmnt", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Statement.");
+            }
+
+            // Execute the proc
+            try (CallableStatement cstmnt = conn.prepareCall("{ ? =call  #msghandler_tmp_proc(?) }")) {
+                cstmnt.registerOutParameter(1, Types.INTEGER);
+                cstmnt.registerOutParameter(2, Types.INTEGER);
+                cstmnt.execute();
+                int procReturnCode = cstmnt.getInt(1);
+                int procRowCount = cstmnt.getInt(2);
+
+                assertEquals(1, procReturnCode, "Expecting ReturnCode 1 from the temp procedure.");
+                assertEquals(1, procRowCount, "Expecting procRowCount 1 from the temp procedure.");
+
+                assertEquals(1, getWarningCount("Conn", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'exec proc', at Connection.");
+                assertEquals(0, getWarningCount("CStmnt", cstmnt.getWarnings()),
+                        "Expecting NO SQLWarnings from 'exec proc', at CallableStatement.");
+            }
+
+            // numOfCalls to the message handler should be: 3
+            assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+            // numOfDowngrades in the message handler should be: 1
+            assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler.");
+
+            // numOfDiscards in the message handler should be: 2
+            assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler.");
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+
+    /**
+     * Test message handler with CallableStatement -- and "feedback" messages
+     * <p>
+     * Do a "long running procedure" and check that the message handler receives the "feedback" messages
+     */
+    @Test
+    @Tag(Constants.xAzureSQLDW)
+    public void testMsgHandlerWithProcedureFeedback() throws Exception {
+        try (SQLServerConnection conn = getConnection()) {
+
+            class TestMsgHandler implements ISQLServerMessageHandler {
+                int numOfCalls = 0;
+                Map<String, Long> feedbackMsgTs = new LinkedHashMap<>();
+
+                @Override
+                public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) {
+                    numOfCalls++;
+
+                    if (50_000 == srvErrorOrWarning.getErrorNumber()) {
+                        // System.out.println("DEBUG: testMsgHandlerWithProcedureFeedback.messageHandler(): FEEDBACK: " + srvErrorOrWarning.getErrorMessage());
+                        // Remember when the message was received
+                        feedbackMsgTs.put(srvErrorOrWarning.getErrorMessage(), System.currentTimeMillis());
+                    }
+
+                    return srvErrorOrWarning;
+                }
+            }
+            TestMsgHandler testMsgHandler = new TestMsgHandler();
+
+            // Create a massage handler
+            conn.setServerMessageHandler(testMsgHandler);
+
+            int doSqlLoopCount = 4;
+            // SQL to create procedure
+            String sqlCreateProc = "" + "CREATE PROCEDURE #msghandler_feeback_proc \n" + "AS \n" + "BEGIN \n"
+                    + "    DECLARE @loop_cnt INT = " + doSqlLoopCount + " \n" + "    DECLARE @feedback VARCHAR(255) \n"
+                    + " \n" + "    WHILE (@loop_cnt > 0) \n" + "    BEGIN \n" + "        WAITFOR DELAY '00:00:01' \n"
+                    + " \n"
+                    + "        SET @feedback = 'In proc, still looping... waiting for loop_count to reach 0. loop_count is now at: ' + convert(varchar(10), @loop_cnt) \n"
+                    + "        RAISERROR(@feedback, 0, 1) WITH NOWAIT \n" + " \n"
+                    + "        SET @loop_cnt = @loop_cnt - 1 \n" + "    END \n" + " \n" + "    RETURN @loop_cnt \n"
+                    + "END \n";
+
+            // Create the proc
+            try (Statement stmnt = conn.createStatement()) {
+                stmnt.executeUpdate(sqlCreateProc);
+                assertEquals(0, getWarningCount("Conn", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Connection.");
+                assertEquals(0, getWarningCount("Stmnt", conn.getWarnings()),
+                        "Expecting NO SQLWarnings from 'create proc', at Statement.");
+            }
+
+            // Execute the proc
+            try (CallableStatement cstmnt = conn.prepareCall("{ ? =call  #msghandler_feeback_proc }")) {
+                cstmnt.registerOutParameter(1, Types.INTEGER);
+                cstmnt.execute();
+                int procReturnCode = cstmnt.getInt(1);
+
+                assertEquals(0, procReturnCode, "Unexpected ReturnCode from the temp procedure.");
+
+                assertEquals(0, getWarningCount("conn", conn.getWarnings()),
+                        "Unexpected Number Of SQLWarnings from 'exec proc', at Connection.");
+                assertEquals(doSqlLoopCount, getWarningCount("cstmnt", cstmnt.getWarnings()),
+                        "Unexpected Number Of SQLWarnings from 'exec proc', at CallableStatement.");
+            }
+
+            // numOfCalls to the message handler should be: #
+            assertEquals(doSqlLoopCount, testMsgHandler.numOfCalls, "Number of message calls to the message handler.");
+
+            // Loop all received messages and check that they are within a second (+-200ms)
+            long prevTime = 0;
+            for (Entry<String, Long> entry : testMsgHandler.feedbackMsgTs.entrySet()) {
+                if (prevTime == 0) {
+                    prevTime = entry.getValue();
+                    continue;
+                }
+
+                long msDiff = entry.getValue() - prevTime;
+                if (msDiff < 800 || msDiff > 1200) {
+                    fail("Received Messages is to far apart. They should be approx 1000 ms. msDiff=" + msDiff
+                            + " Message=|" + entry.getKey() + "|.");
+                }
+
+                prevTime = entry.getValue();
+            }
+
+        } catch (SQLException ex) {
+            fail(TestResource.getResource("R_unexpectedErrorMessage") + ex.getMessage());
+        }
+    }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
index d17fa146..1ce9cd19 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
@@ -62,6 +62,9 @@ public abstract class AbstractTest {
     protected static String tenantID;
     protected static String[] keyIDs = null;
 
+    protected static String linkedServer = null;
+    protected static String remoteUser = null;
+    protected static String remotePassword = null;
     protected static String[] enclaveServer = null;
     protected static String[] enclaveAttestationUrl = null;
     protected static String[] enclaveAttestationProtocol = null;
@@ -197,6 +200,10 @@ public abstract class AbstractTest {
 
         clientKeyPassword = getConfiguredProperty("clientKeyPassword", "");
 
+        linkedServer = getConfiguredProperty("linkedServer", null);
+        remoteUser = getConfiguredProperty("remoteUser", null);
+        remotePassword = getConfiguredProperty("remotePassword", null);
+
         kerberosServer = getConfiguredProperty("kerberosServer", null);
         kerberosServerPort = getConfiguredProperty("kerberosServerPort", null);