Skip to content

FIX: handle TransactionManager (0x0E) requests in mock TDS server - #87

Merged
saurabh500 merged 1 commit into
mainfrom
dev/saurabh/mock-tds-transaction-manager
Jul 1, 2026
Merged

FIX: handle TransactionManager (0x0E) requests in mock TDS server#87
saurabh500 merged 1 commit into
mainfrom
dev/saurabh/mock-tds-transaction-manager

Conversation

@saurabh500

@saurabh500 saurabh500 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #86.

Python DB-API drivers such as mssql-python and pyodbc default to autocommit=OFF. After a successful FedAuth Login7, the ODBC driver immediately issues a TransactionManager request (TDS packet type 0x0E) to begin a transaction. The mock TDS server did not recognize 0x0E, so PacketHeader::parse failed with Invalid packet type: 14, no response was written, and the client blocked forever waiting for the transaction reply.

This showed up as a 1-hour CI worker timeout on Linux in the mssql-python mock-TDS FedAuth tests (the Windows login-timeout behavior masked it there).

Changes

  • Add PacketType::TransactionManager = 0x0E to the PacketType enum and its TryFrom<u8>.
  • Add parse_transaction_manager_request to read the RequestType (BEGIN/COMMIT/ROLLBACK) from the request body (skips the ALL_HEADERS block).
  • Add build_transaction_manager_response to emit a matching transaction EnvChange (Begin=8, Commit=9, Rollback=10) plus a Done token so connection setup completes cleanly.
  • Handle TransactionManager in both dispatch paths: the TLS process_packet path and the plaintext handle_connection path.
  • Add unit tests for the parse/build helpers (cargo test now runs 16 tests).

Validation

  • cargo fmt --check, cargo clippy, and cargo test -p mssql-mock-tds all pass.
  • Reproduced the original hang, then confirmed the fix end-to-end: an mssql-python connect against the fixed mock now logs Handling TransactionManager request (type 5), sends a response, and the client connects and closes cleanly instead of hanging. The FedAuth access token is recorded as before.

Python DB-API drivers (mssql-python, pyodbc) default to autocommit=OFF,
so after a successful FedAuth Login7 the client issues a TransactionManager
request to begin a transaction. The mock did not recognize packet type
0x0E, so PacketHeader::parse failed ("Invalid packet type: 14"), no
response was written, and the client blocked forever waiting for the
transaction reply. This surfaced as a 1-hour CI worker timeout on Linux,
where the login timeout is not honored.

Add PacketType::TransactionManager (0x0E), parse the RequestType from the
request body, and reply with a matching transaction EnvChange + Done token
so connection setup completes cleanly. Handled in both the TLS
(process_packet) and plaintext (handle_connection) dispatch paths, with
unit tests for parse/build helpers.

Fixes #86

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the mock TDS server (mssql-mock-tds) to properly recognize and respond to TransactionManager (0x0E) packets, preventing autocommit-off clients (e.g., pyodbc, mssql-python) from hanging immediately after a successful FedAuth Login7.

Changes:

  • Added PacketType::TransactionManager = 0x0E and decoding support in TryFrom<u8>.
  • Introduced parse_transaction_manager_request and build_transaction_manager_response helpers to parse the request type and emit an EnvChange + Done token stream.
  • Handled TransactionManager packets in both server dispatch paths (TLS process_packet and plaintext handle_connection) and added unit tests for the new helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
mssql-mock-tds/src/server.rs Adds TransactionManager handling in both TLS and plaintext packet dispatch so clients receive a response instead of hanging.
mssql-mock-tds/src/protocol.rs Extends packet type decoding and adds parse/build helpers plus unit tests for TransactionManager request/response behavior.

Comment thread mssql-mock-tds/src/protocol.rs
@saurabh500
saurabh500 marked this pull request as ready for review July 1, 2026 00:33
@saurabh500
saurabh500 requested a review from a team as a code owner July 1, 2026 00:33
@saurabh500
saurabh500 enabled auto-merge (squash) July 1, 2026 00:35
@saurabh500
saurabh500 merged commit 2f759d1 into main Jul 1, 2026
15 of 16 checks passed
@saurabh500
saurabh500 deleted the dev/saurabh/mock-tds-transaction-manager branch July 1, 2026 01:19
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

0%

🎯 Overall Coverage

91.4%

📦 Project: mssql-tds
ℹ️ Note: diff coverage is reported, not enforced.


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql-mock-tds/src/protocol.rs (0.0%): Missing lines 57,704-707,709-714,717-719,721-725,732-733,741-745,748,750-762,764-767,769-771
  • mssql-mock-tds/src/server.rs (0.0%): Missing lines 386-388,393,395-399,401

Summary

  • Total: 57 lines
  • Missing: 57 lines
  • Coverage: 0%

mssql-mock-tds/src/protocol.rs

  53             0x04 => Ok(PacketType::TabularResult),
  54             0x06 => Ok(PacketType::Attention),
  55             0x10 => Ok(PacketType::Login7),
  56             0x03 => Ok(PacketType::RpcRequest),
! 57             0x0E => Ok(PacketType::TransactionManager),
  58             _ => Err(ProtocolError::InvalidPacketType(value)),
  59         }
  60     }
  61 }

  700 ///
  701 /// The body begins with an ALL_HEADERS block (a u32 little-endian total length
  702 /// followed by that many bytes of headers), after which comes the u16
  703 /// little-endian RequestType. Returns `None` if the body is too short.
! 704 pub fn parse_transaction_manager_request(packet_body: &[u8]) -> Option<u16> {
! 705     if packet_body.len() < 4 {
! 706         return None;
! 707     }
  708 
! 709     let all_headers_len = u32::from_le_bytes([
! 710         packet_body[0],
! 711         packet_body[1],
! 712         packet_body[2],
! 713         packet_body[3],
! 714     ]) as usize;
  715 
  716     // RequestType is a u16 immediately after the ALL_HEADERS block.
! 717     if packet_body.len() < all_headers_len + 2 {
! 718         return None;
! 719     }
  720 
! 721     Some(u16::from_le_bytes([
! 722         packet_body[all_headers_len],
! 723         packet_body[all_headers_len + 1],
! 724     ]))
! 725 }
  726 
  727 /// Build the token stream (EnvChange + Done) that answers a TransactionManager
  728 /// request. Begin/commit/rollback requests get a matching transaction EnvChange
  729 /// so the client's connection setup can complete; any other request just gets a

  728 /// request. Begin/commit/rollback requests get a matching transaction EnvChange
  729 /// so the client's connection setup can complete; any other request just gets a
  730 /// Done token. The returned bytes are the raw tokens and still need to be
  731 /// wrapped in a TabularResult packet by the caller.
! 732 pub fn build_transaction_manager_response(request_type: u16) -> BytesMut {
! 733     let mut tokens = BytesMut::new();
  734 
  735     // Fixed 8-byte transaction descriptor handed to the client. Any non-zero
  736     // value works for the mock; the client only echoes it back in the
  737     // ALL_HEADERS of subsequent requests.

  737     // ALL_HEADERS of subsequent requests.
  738     const XACT_DESCRIPTOR: [u8; 8] = [1, 0, 0, 0, 0, 0, 0, 0];
  739 
  740     // EnvChange transaction sub-types.
! 741     let env_type = match request_type {
! 742         TM_BEGIN_XACT => Some(8u8),     // Begin Transaction
! 743         TM_COMMIT_XACT => Some(9u8),    // Commit Transaction
! 744         TM_ROLLBACK_XACT => Some(10u8), // Rollback Transaction
! 745         _ => None,
  746     };
  747 
! 748     if let Some(env_type) = env_type {
  749         // Build the EnvChange payload first so we can prefix its length.
! 750         let mut payload = BytesMut::new();
! 751         payload.put_u8(env_type);
! 752         if env_type == 8 {
! 753             // Begin: NewValue = descriptor, OldValue = empty.
! 754             payload.put_u8(XACT_DESCRIPTOR.len() as u8);
! 755             payload.put_slice(&XACT_DESCRIPTOR);
! 756             payload.put_u8(0);
! 757         } else {
! 758             // Commit/Rollback: NewValue = empty, OldValue = descriptor.
! 759             payload.put_u8(0);
! 760             payload.put_u8(XACT_DESCRIPTOR.len() as u8);
! 761             payload.put_slice(&XACT_DESCRIPTOR);
! 762         }
  763 
! 764         tokens.put_u8(TokenType::EnvChange as u8);
! 765         tokens.put_u16_le(payload.len() as u16);
! 766         tokens.extend_from_slice(&payload);
! 767     }
  768 
! 769     tokens.extend_from_slice(&build_done_token(0));
! 770     tokens
! 771 }
  772 
  773 /// Build an INFO token (0xAB) matching the TDS wire format.
  774 ///
  775 /// Wire layout (all integers little-endian):

mssql-mock-tds/src/server.rs

  382                 // Clients that connect with autocommit disabled (the default for
  383                 // Python DB-API drivers) issue a TransactionManager request to
  384                 // begin a transaction right after login. Answer it so their
  385                 // connection setup can complete instead of blocking forever.
! 386                 let packet_body = &packet_data[PACKET_HEADER_SIZE..];
! 387                 let request_type = parse_transaction_manager_request(packet_body).unwrap_or(0);
! 388                 debug!(
  389                     "Handling TransactionManager request (type {}) from {}",
  390                     request_type, self.addr
  391                 );
  392 
! 393                 let tokens = build_transaction_manager_response(request_type);
  394 
! 395                 let total_length = (PACKET_HEADER_SIZE + tokens.len()) as u16;
! 396                 let mut packet = BytesMut::with_capacity(total_length as usize);
! 397                 let resp_header = PacketHeader::new(PacketType::TabularResult, total_length, 1);
! 398                 resp_header.write(&mut packet);
! 399                 packet.extend_from_slice(&tokens);
  400 
! 401                 Some(packet)
  402             }
  403 
  404             _ => {
  405                 debug!(


🔗 Quick Links

View Azure DevOps Build · Coverage Report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mock TDS server hangs autocommit-off clients: TransactionManager (0x0E) requests are unhandled

3 participants