From 42443c0340f54d9441e887367473913729e25436 Mon Sep 17 00:00:00 2001 From: raphaelroshan Date: Tue, 14 Jul 2026 19:56:06 +0800 Subject: [PATCH] Fix duplicate 10= checksum on resend of message ending in a repeating group When a stored message whose body ends in a repeating group is parsed with a DataDictionary, parseGroup set trailerBytes to the (now-empty) rawBytes after consuming the trailer field, so the caller's bodyBytes strip removed nothing and left the 10= trailer embedded in bodyBytes. On resend, buildWithBodyBytes wrote that contaminated bodyBytes and appended a fresh 10=, producing two checksum tags and a wrong BodyLength, which the counterparty rejects. parseGroup now restores the pre-extract buffer (which still holds the trailer) when it detects the trailer field, so the strip removes it as in the non-group path. Adds a dictionary-based resend test covering a body that ends in a group. --- message.go | 5 +++++ message_test.go | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/message.go b/message.go index 5bea1d375..69407cf5d 100644 --- a/message.go +++ b/message.go @@ -313,6 +313,7 @@ func parseGroup(mp *msgParser, tags []Tag) { for { mp.fieldIndex++ mp.parsedFieldBytes = &mp.msg.fields[mp.fieldIndex] + preExtractBytes := mp.rawBytes mp.rawBytes, _ = extractField(mp.parsedFieldBytes, mp.rawBytes) mp.trailerBytes = mp.rawBytes @@ -337,6 +338,10 @@ func parseGroup(mp *msgParser, tags []Tag) { mp.msg.Body.add(dm) mp.msg.Trailer.add(mp.msg.fields[mp.fieldIndex : mp.fieldIndex+1]) mp.foundTrailer = true + // Restore the buffer that still holds the trailer so the caller strips + // it from bodyBytes; otherwise the trailer stays embedded and resend + // emits a duplicate 10= tag. + mp.trailerBytes = preExtractBytes break } else { // Found a body field outside the group. diff --git a/message_test.go b/message_test.go index 72f11dcd0..a51c5231e 100644 --- a/message_test.go +++ b/message_test.go @@ -448,6 +448,26 @@ func (s *MessageSuite) TestReBuildWithRepeatingGroupMultipleEntriesInGroupForRes s.True(bytes.Equal(expectedResendBytes, resendBytes), "Unexpected bytes,\n expected: %s\n but was: %s", expectedResendBytes, resendBytes) } +func (s *MessageSuite) TestReBuildWithRepeatingGroupWithDictionaryForResend() { + dict, dictErr := datadictionary.Parse("spec/FIX44.xml") + s.Nil(dictErr) + + // A message whose body ends in a repeating group (453), parsed WITH a + // dictionary so parseGroup handles the group. + rawMsg := bytes.NewBufferString( + "8=FIX.4.4\x019=165\x0135=D\x0134=2\x0149=01001\x0150=01001a\x0152=20231231-20:19:41\x0156=TEST\x01" + + "1=acct1\x0111=13976\x0121=1\x0138=1\x0140=2\x0144=12\x0154=1\x0155=SYMABC\x0159=0\x0160=20231231-20:19:41\x01453=1\x01448=4501\x01447=D\x01452=28\x01" + + "10=026\x01") + s.Nil(ParseMessageWithDataDictionary(s.msg, rawMsg, dict, dict)) + + // The trailer must not stay embedded in bodyBytes. + s.False(bytes.Contains(s.msg.bodyBytes, []byte("\x0110=")), "trailer leaked into bodyBytes: %s", s.msg.bodyBytes) + + // A resend rebuilt from bodyBytes must carry exactly one 10= checksum. + resendBytes := s.msg.buildWithBodyBytes(s.msg.bodyBytes) + s.Equal(1, bytes.Count(resendBytes, []byte("\x0110=")), "expected exactly one 10= checksum, got: %s", resendBytes) +} + func (s *MessageSuite) TestReverseRoute() { s.Nil(ParseMessage(s.msg, bytes.NewBufferString("8=FIX.4.29=17135=D34=249=TW50=KK52=20060102-15:04:0556=ISLD57=AP144=BB115=JCD116=CS128=MG129=CB142=JV143=RY145=BH11=ID21=338=10040=w54=155=INTC60=20060102-15:04:0510=123")))