@@ -722,4 +722,148 @@ public void nonRecoverableThenRecoverableErrorStopsPolling() throws Exception {
722722 executor .shutdown ();
723723 }
724724 }
725+
726+ @ Test
727+ public void internalErrorWithInvalidDataKindContinuesPolling () throws Exception {
728+ FDv2Requestor requestor = mockRequestor ();
729+ SelectorSource selectorSource = mockSelectorSource ();
730+ ScheduledExecutorService executor = Executors .newScheduledThreadPool (1 );
731+
732+ AtomicInteger callCount = new AtomicInteger (0 );
733+ when (requestor .Poll (any (Selector .class ))).thenAnswer (invocation -> {
734+ int count = callCount .incrementAndGet ();
735+ if (count == 1 ) {
736+ // First call returns response with malformed put-object which triggers INTERNAL_ERROR (INVALID_DATA)
737+ String malformedPutObjectJson = "{\n " +
738+ " \" events\" : [\n " +
739+ " {\n " +
740+ " \" event\" : \" server-intent\" ,\n " +
741+ " \" data\" : {\n " +
742+ " \" payloads\" : [{\n " +
743+ " \" id\" : \" payload-1\" ,\n " +
744+ " \" target\" : 100,\n " +
745+ " \" intentCode\" : \" xfer-full\" ,\n " +
746+ " \" reason\" : \" payload-missing\" \n " +
747+ " }]\n " +
748+ " }\n " +
749+ " },\n " +
750+ " {\n " +
751+ " \" event\" : \" payload-transferred\" ,\n " +
752+ " \" data\" : {\n " +
753+ " \" state\" : \" (p:payload-1:100)\" ,\n " +
754+ " \" version\" : 100\n " +
755+ " }\n " +
756+ " },\n " +
757+ " {\n " +
758+ " \" event\" : \" put-object\" ,\n " +
759+ " \" data\" : {}\n " +
760+ " }\n " +
761+ " ]\n " +
762+ "}" ;
763+
764+ return CompletableFuture .completedFuture (new FDv2Requestor .FDv2PayloadResponse (
765+ com .launchdarkly .sdk .internal .fdv2 .payloads .FDv2Event .parseEventsArray (malformedPutObjectJson ),
766+ okhttp3 .Headers .of ()
767+ ));
768+ } else {
769+ // Subsequent calls succeed
770+ return CompletableFuture .completedFuture (makeSuccessResponse ());
771+ }
772+ });
773+
774+ try {
775+ PollingSynchronizerImpl synchronizer = new PollingSynchronizerImpl (
776+ requestor ,
777+ testLogger ,
778+ selectorSource ,
779+ executor ,
780+ Duration .ofMillis (50 )
781+ );
782+
783+ // Wait for multiple polls
784+ Thread .sleep (250 );
785+
786+ // First result should be interrupted with INVALID_DATA error kind
787+ FDv2SourceResult result1 = synchronizer .next ().get (1 , TimeUnit .SECONDS );
788+ assertNotNull (result1 );
789+ assertEquals (FDv2SourceResult .ResultType .STATUS , result1 .getResultType ());
790+ assertEquals (FDv2SourceResult .State .INTERRUPTED , result1 .getStatus ().getState ());
791+ assertEquals (DataSourceStatusProvider .ErrorKind .INVALID_DATA , result1 .getStatus ().getErrorInfo ().getKind ());
792+
793+ // Second result should be success (polling continued)
794+ FDv2SourceResult result2 = synchronizer .next ().get (1 , TimeUnit .SECONDS );
795+ assertNotNull (result2 );
796+ assertEquals (FDv2SourceResult .ResultType .CHANGE_SET , result2 .getResultType ());
797+
798+ // Verify polling continued after internal error
799+ assertTrue ("Should have made at least 2 calls" , callCount .get () >= 2 );
800+
801+ synchronizer .close ();
802+ } finally {
803+ executor .shutdown ();
804+ }
805+ }
806+
807+ @ Test
808+ public void internalErrorWithUnknownKindContinuesPolling () throws Exception {
809+ FDv2Requestor requestor = mockRequestor ();
810+ SelectorSource selectorSource = mockSelectorSource ();
811+ ScheduledExecutorService executor = Executors .newScheduledThreadPool (1 );
812+
813+ AtomicInteger callCount = new AtomicInteger (0 );
814+ when (requestor .Poll (any (Selector .class ))).thenAnswer (invocation -> {
815+ int count = callCount .incrementAndGet ();
816+ if (count == 1 ) {
817+ // First call returns response with unknown event which triggers INTERNAL_ERROR (UNKNOWN)
818+ String unknownEventJson = "{\n " +
819+ " \" events\" : [\n " +
820+ " {\n " +
821+ " \" event\" : \" unrecognized-event-type\" ,\n " +
822+ " \" data\" : {}\n " +
823+ " }\n " +
824+ " ]\n " +
825+ "}" ;
826+
827+ return CompletableFuture .completedFuture (new FDv2Requestor .FDv2PayloadResponse (
828+ com .launchdarkly .sdk .internal .fdv2 .payloads .FDv2Event .parseEventsArray (unknownEventJson ),
829+ okhttp3 .Headers .of ()
830+ ));
831+ } else {
832+ // Subsequent calls succeed
833+ return CompletableFuture .completedFuture (makeSuccessResponse ());
834+ }
835+ });
836+
837+ try {
838+ PollingSynchronizerImpl synchronizer = new PollingSynchronizerImpl (
839+ requestor ,
840+ testLogger ,
841+ selectorSource ,
842+ executor ,
843+ Duration .ofMillis (50 )
844+ );
845+
846+ // Wait for multiple polls
847+ Thread .sleep (250 );
848+
849+ // First result should be interrupted with UNKNOWN error kind
850+ FDv2SourceResult result1 = synchronizer .next ().get (1 , TimeUnit .SECONDS );
851+ assertNotNull (result1 );
852+ assertEquals (FDv2SourceResult .ResultType .STATUS , result1 .getResultType ());
853+ assertEquals (FDv2SourceResult .State .INTERRUPTED , result1 .getStatus ().getState ());
854+ assertEquals (DataSourceStatusProvider .ErrorKind .UNKNOWN , result1 .getStatus ().getErrorInfo ().getKind ());
855+
856+ // Second result should be success (polling continued)
857+ FDv2SourceResult result2 = synchronizer .next ().get (1 , TimeUnit .SECONDS );
858+ assertNotNull (result2 );
859+ assertEquals (FDv2SourceResult .ResultType .CHANGE_SET , result2 .getResultType ());
860+
861+ // Verify polling continued after internal error
862+ assertTrue ("Should have made at least 2 calls" , callCount .get () >= 2 );
863+
864+ synchronizer .close ();
865+ } finally {
866+ executor .shutdown ();
867+ }
868+ }
725869}
0 commit comments