Summary
The mock TDS server (mssql-mock-tds / mssql-mock-tds-py) hangs any client that connects with autocommit disabled. After a successful FedAuth login, such clients send a TransactionManager (0x0E) request to begin a transaction. The mock does not recognize packet type 0x0E, so it silently drops the request and never replies — the client then blocks indefinitely waiting for the transaction response.
This makes the mock unusable for driver integration tests on Linux, where connect/login timeouts are not always honored against a half-responsive server, so the client hangs forever instead of erroring out.
Impact
We are adding mock-TDS FedAuth regression tests to mssql-python (regressions like microsoft/mssql-python#596). The tests hang on Linux CI (hit the 1-hour worker timeout). They only "pass" on Windows because there the ODBC login timeout is honored, so the still-open connection is eventually torn down.
Both mssql-python and pyodbc reproduce this — because Python DB-API drivers default to autocommit = OFF, so the ODBC Driver 18 begins a transaction immediately after login and issues a TransactionManager request.
Root cause
mssql-mock-tds/src/protocol.rs — the PacketType enum / TryFrom<u8> does not include 0x0E (TransactionManager). In server.rs::process_packet, PacketHeader::parse therefore fails, process_packet returns Ok(None), and no response is written. The read loop then blocks on the next read_buf, and the client blocks waiting for the transaction reply.
Evidence (mock trace log, TDS 7.4 / optional TLS, ODBC Driver 18 on Linux)
INFO ...server: TLS handshake successful for 127.0.0.1:56527
DEBUG ...server: Handling Login7 from 127.0.0.1:56527
DEBUG ...protocol: Parsed access token from Login7: 36 bytes
DEBUG ...server: Stored access token (36 bytes) from 127.0.0.1:56527 for verification
DEBUG ...server: Sending 118 encrypted bytes response # LoginAck + FeatureExtAck + Done
DEBUG ...server: Received 34 encrypted bytes from 127.0.0.1:56527 (TDS-wrapped TLS)
WARN ...server: Failed to parse packet header: Invalid packet type: 14 # 14 == 0x0E TransactionManager
# ... no response sent; client blocks until it is force-closed 14s later
DEBUG ...server: TLS connection closed by client 127.0.0.1:56527
The FedAuth token is parsed correctly; the connection simply never completes because the TransactionManager request goes unanswered.
Requested change
-
Handle TransactionManager (0x0E) requests. Add TransactionManager = 0x0E to PacketType and its TryFrom<u8>, and add a process_packet arm that:
- Parses the
RequestType (u16 LE) that follows the leading ALL_HEADERS block (u32 LE total length + that many header bytes).
- Replies with an appropriate token stream so the client can proceed, e.g.:
TM_BEGIN_XACT (5) → ENVCHANGE type 8 (Begin Transaction) carrying an 8-byte transaction descriptor + DONE.
TM_COMMIT_XACT (7) → ENVCHANGE type 9 (Commit) + DONE.
TM_ROLLBACK_XACT (8) → ENVCHANGE type 10 (Rollback) + DONE.
- anything else →
DONE.
- Wrap the tokens in a
TabularResult (0x04) packet, like the Login7/SqlBatch paths do.
This lets an autocommit-off connection complete its BEGIN TRAN handshake, so connect() returns successfully on all platforms and the connection closes cleanly.
-
(Optional, nice-to-have) Record received token as soon as it is parsed. Today ConnectionStore is only populated after the per-connection read loop exits (i.e. after the client disconnects). Storing the token into the shared store at Login7 time would let tests observe has_received_token() even while the connection is still open, and makes the store resilient to clients that keep the connection alive.
Repro
- Mock: TDS 7.4 optional-TLS mode (
--tls-mode optional --cert ... --key ..., or PyMockTdsServer(port=0, tls=True)).
- Client:
mssql-python (or pyodbc) with ODBC Driver 18 on Linux, connecting with an access token via SQL_COPT_SS_ACCESS_TOKEN and default autocommit (OFF). The connect call never returns.
Summary
The mock TDS server (
mssql-mock-tds/mssql-mock-tds-py) hangs any client that connects with autocommit disabled. After a successful FedAuth login, such clients send a TransactionManager (0x0E) request to begin a transaction. The mock does not recognize packet type0x0E, so it silently drops the request and never replies — the client then blocks indefinitely waiting for the transaction response.This makes the mock unusable for driver integration tests on Linux, where connect/login timeouts are not always honored against a half-responsive server, so the client hangs forever instead of erroring out.
Impact
We are adding mock-TDS FedAuth regression tests to
mssql-python(regressions like microsoft/mssql-python#596). The tests hang on Linux CI (hit the 1-hour worker timeout). They only "pass" on Windows because there the ODBC login timeout is honored, so the still-open connection is eventually torn down.Both
mssql-pythonandpyodbcreproduce this — because Python DB-API drivers default to autocommit = OFF, so the ODBC Driver 18 begins a transaction immediately after login and issues a TransactionManager request.Root cause
mssql-mock-tds/src/protocol.rs— thePacketTypeenum /TryFrom<u8>does not include0x0E(TransactionManager). Inserver.rs::process_packet,PacketHeader::parsetherefore fails,process_packetreturnsOk(None), and no response is written. The read loop then blocks on the nextread_buf, and the client blocks waiting for the transaction reply.Evidence (mock trace log, TDS 7.4 / optional TLS, ODBC Driver 18 on Linux)
The FedAuth token is parsed correctly; the connection simply never completes because the TransactionManager request goes unanswered.
Requested change
Handle TransactionManager (
0x0E) requests. AddTransactionManager = 0x0EtoPacketTypeand itsTryFrom<u8>, and add aprocess_packetarm that:RequestType(u16 LE) that follows the leadingALL_HEADERSblock (u32 LE total length + that many header bytes).TM_BEGIN_XACT(5) →ENVCHANGEtype 8 (Begin Transaction) carrying an 8-byte transaction descriptor +DONE.TM_COMMIT_XACT(7) →ENVCHANGEtype 9 (Commit) +DONE.TM_ROLLBACK_XACT(8) →ENVCHANGEtype 10 (Rollback) +DONE.DONE.TabularResult(0x04) packet, like the Login7/SqlBatch paths do.This lets an autocommit-off connection complete its
BEGIN TRANhandshake, soconnect()returns successfully on all platforms and the connection closes cleanly.(Optional, nice-to-have) Record received token as soon as it is parsed. Today
ConnectionStoreis only populated after the per-connection read loop exits (i.e. after the client disconnects). Storing the token into the shared store at Login7 time would let tests observehas_received_token()even while the connection is still open, and makes the store resilient to clients that keep the connection alive.Repro
--tls-mode optional --cert ... --key ..., orPyMockTdsServer(port=0, tls=True)).mssql-python(orpyodbc) with ODBC Driver 18 on Linux, connecting with an access token viaSQL_COPT_SS_ACCESS_TOKENand default autocommit (OFF). The connect call never returns.