Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 27, 2026

Fix: Prevent incorrect resend of session-level messages when ForceResendWhenCorruptedStore is enabled

Progress:

  • Understand the problem and current implementation
  • Modify resendMessages() in Session.java to:
    • Remove dependency on forceResendWhenCorruptedStore flag for admin message detection
    • Add special handling for Reject messages (MsgType=3) - should be resent
    • Properly skip other session-level messages with gap fill
    • Ensure application messages are resent normally
    • Keep original Heartbeat handling for corrupted store (per review feedback)
    • Use session-specific logging instead of global LOG (per review feedback)
  • Create unit test in SessionTest.java to verify fix
  • Add setter in SessionFactoryTestSupport.Builder for forceResendWhenCorruptedStore
  • Run SessionTest suite - all 72 tests pass
  • Address review feedback

Summary

✅ Core fix for admin message handling maintained
✅ Original Heartbeat logic for corrupted store restored
✅ Session-specific logging used throughout
✅ All session tests pass (72/72)

Original prompt

Problem Description

When the option ForceResendWhenCorruptedStore is enabled and a ResendRequest is received, session-level messages (admin messages) are incorrectly being resent. This violates the FIX specification, which states that Reject messages are the only session-level messages that must be resent. Other session-level messages like Logon, Logout, and Heartbeat should be skipped using SequenceReset GapFill messages.

Reference issue: #597

Root Cause

The bug is located in the resendMessages() method in quickfixj-core/src/main/java/quickfix/Session.java around lines 2367-2386.

The problematic code is:

if (MessageUtils.isAdminMessage(msgType) && !forceResendWhenCorruptedStore) {
    if (begin == 0) {
        begin = msgSeqNum;
    }
} else {
    initializeResendFields(msg);
    if (resendApproved(msg)) {
        // ... resends the message
    }
}

When forceResendWhenCorruptedStore is true, the condition MessageUtils.isAdminMessage(msgType) && !forceResendWhenCorruptedStore evaluates to false, causing ALL messages (including session-level ones) to fall into the else branch where they are resent. This also incorrectly calls toApp() on session-level messages.

Expected Behavior

According to the FIX specification:

  1. Only Reject messages among session-level messages should be resent
  2. All other session-level messages (Logon, Logout, Heartbeat, TestRequest, etc.) should NOT be resent
  3. Instead, SequenceReset GapFill messages should be sent to skip over these sequence numbers
  4. Application messages should continue to be resent normally

Solution

Modify the resendMessages() method to:

  1. Remove the dependency on forceResendWhenCorruptedStore flag when determining whether to resend admin messages
  2. Add special handling for Reject messages: Among session-level messages, only Reject (MsgType=3) should be resent
  3. Properly skip other session-level messages with SequenceReset GapFill
  4. Improve corrupted store handling: When the store is corrupted, use placeholders instead of fake Heartbeat messages, then properly gap fill over missing messages

Changes Required

File: quickfixj-core/src/main/java/quickfix/Session.java

In the resendMessages() method (around lines 2320-2411), update the logic to:

  1. Handle corrupted store by adding null placeholders instead of creating fake Heartbeat messages
  2. Check if messages are admin messages and handle appropriately:
    • If it's a Reject message: resend it (call initializeResendFields, resendApproved, and send)
    • If it's any other session-level message: mark for gap fill (set begin if needed)
    • If it's an application message: resend it normally
  3. Ensure proper SequenceReset GapFill generation for skipped messages

Test Case

Create a unit test in quickfixj-core/src/test/java/quickfix/SessionTest.java that:

  1. Enables ForceResendWhenCorruptedStore option
  2. Stores some session-level messages (Logon, Heartbeat) and application messages in the message store
  3. Triggers a ResendRequest
  4. Verifies that:
    • Application messages are resent with PossDupFlag
    • Session-level messages (except Reject) are NOT resent
    • SequenceReset GapFill messages are sent to skip session-level message sequence numbers
    • toApp() is not called on session-level messages

Benefits

  • Compliant with FIX specification
  • Session-level messages (except Reject) will no longer be incorrectly resent
  • toApp() will not be called on session-level messages during resend
  • Proper SequenceReset GapFill messages will be generated
  • Better handling of corrupted message stores

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 27, 2026 11:37
…tore is enabled

- Modified Session.resendMessages() to remove dependency on forceResendWhenCorruptedStore flag for admin message detection
- Added special handling for Reject messages (MsgType=3) - only Reject messages among admin messages are now resent
- Other session-level messages (Logon, Logout, Heartbeat, etc.) are properly skipped with SequenceReset GapFill
- Handle corrupted store by adding null placeholders instead of fake Heartbeat messages
- Added comprehensive test to verify the fix
- Added setter for forceResendWhenCorruptedStore in SessionFactoryTestSupport.Builder

Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com>
Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix incorrect resending of session-level messages Fix incorrect resend of session-level messages with ForceResendWhenCorruptedStore Jan 27, 2026
Copilot AI requested a review from chrjohn January 27, 2026 11:42
@chrjohn chrjohn changed the title Fix incorrect resend of session-level messages with ForceResendWhenCorruptedStore Fix incorrect resend of session-level messages with ForceResendWhenCorruptedStore Jan 27, 2026
Comment on lines 2355 to 2358
LOG.error("Cannot read messages from stores, will gap fill over missing messages", e);
// Add null placeholders for corrupted messages instead of fake Heartbeats
for (int i = beginSeqNo; i < endSeqNo; i++) {
final Message heartbeat = messageFactory.create(sessionID.getBeginString(),
MsgType.HEARTBEAT);
initializeHeader(heartbeat.getHeader());
heartbeat.getHeader().setInt(MsgSeqNum.FIELD, i);
messages.add(heartbeat.toString());
messages.add(null);
Copy link
Member

Choose a reason for hiding this comment

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

@copilot please roll back these changes and restore the former logic that puts Heartbeats into the messages list.
Moreover, use the session-specific log instead of the global LOG.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in e180fa4. Restored the original logic that creates Heartbeat messages and changed to use getLog().onErrorEvent() for session-specific logging.

Comment on lines 2375 to 2383
// Handle null placeholders from corrupted store
if (message == null) {
// Mark for gap fill
if (begin == 0) {
begin = current;
}
current++;
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

@copilot this can be removed also.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in e180fa4. Removed the null placeholder handling code.

…ific log

- Restored original corrupted store handling that creates Heartbeat messages instead of null placeholders
- Changed LOG.error to getLog().onErrorEvent to use session-specific logging
- Removed null placeholder handling code
- Kept the core fix for admin message resend logic (Reject messages handling)

Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com>
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.

ForceResendWhenCorruptedStore causes session-level messages to be resent

2 participants