@@ -860,6 +860,38 @@ describe('Retry Plugin', function() {
860860 } ) ;
861861
862862 describe ( 'retries on error' , function ( ) {
863+ function dataPhaseErrorMockServer ( port , timesToError , successBody , lifetime , done ) {
864+ // nock is unable to mock the timeout in the response phase so create a mock http server
865+ const http = require ( 'http' ) ;
866+ var counter = 0 ;
867+ const mockServer = http . createServer ( { } , function ( req , res ) {
868+ counter ++ ;
869+ console . log ( `DPE MOCK SERVER: received request ${ counter } for URL: ${ req . url } ` ) ;
870+ if ( counter <= timesToError ) {
871+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
872+ res . write ( successBody ) ;
873+ // We intentionally fail to end the request so that the client side will timeout in the data phase
874+ // res.end();
875+ } else {
876+ // respond successfully promptly
877+ res . end ( successBody ) ;
878+ }
879+ } ) . listen ( port , '127.0.0.1' ) ;
880+ // set a timeout so we don't hang the test (mocha's timeout won't kill the http server)
881+ const serverTimeout = setTimeout ( ( ) => {
882+ console . log ( 'DPE MOCK SERVER: timeout' ) ;
883+ if ( mockServer . listening ) {
884+ console . log ( 'DPE MOCK SERVER: close requested from timeout' ) ;
885+ mockServer . close ( ( ) => done ( new Error ( 'Test server timeout; client hang!' ) ) ) ;
886+ }
887+ } , lifetime ) ;
888+ return { server : mockServer ,
889+ close : ( ) => {
890+ clearTimeout ( serverTimeout ) ;
891+ mockServer . close ( done ) ;
892+ } } ;
893+ }
894+
863895 describe ( 'with callback only' , function ( ) {
864896 it ( 'performs request and returns response' , function ( done ) {
865897 // NOTE: Use NOCK_OFF=true to test using a real CouchDB instance.
@@ -945,6 +977,31 @@ describe('Retry Plugin', function() {
945977 done ( ) ;
946978 } ) ;
947979 } ) ;
980+
981+ it ( 'does not retry in response data phase' , function ( done ) {
982+ // mock server on 6984 that errors for 1 request then returns the DB info block for other reqs
983+ // and finally stops listening after 1s if it wasn't already closed
984+ const mock = dataPhaseErrorMockServer ( 6984 , 1 , '{"doc_count":0}' , 1000 , done ) ;
985+ // A client that will timeout after 100 ms and make only 2 attempts with a retry 10 ms after receiving an error
986+ var cloudantClient = new Client ( {
987+ https : false ,
988+ maxAttempt : 2 ,
989+ requestDefaults : { timeout : 100 } ,
990+ plugins : { retry : { retryInitialDelayMsecs : 10 , retryStatusCodes : [ ] } }
991+ } ) ;
992+
993+ var req = {
994+ url : 'http://127.0.0.1:6984/foo' ,
995+ auth : { username : ME , password : PASSWORD } ,
996+ method : 'GET'
997+ } ;
998+
999+ cloudantClient . request ( req , function ( err , resp , data ) {
1000+ assert . ok ( err , 'Should get called back with an error.' ) ;
1001+ assert . equal ( 'ESOCKETTIMEDOUT' , err . code ) ;
1002+ mock . close ( ) ;
1003+ } ) ;
1004+ } ) ;
9481005 } ) ;
9491006
9501007 describe ( 'with listener only' , function ( ) {
@@ -1084,6 +1141,54 @@ describe('Retry Plugin', function() {
10841141 assert . fail ( 'Unexpected data from server' ) ;
10851142 } ) ;
10861143 } ) ;
1144+
1145+ it ( 'does not retry in response data phase' , function ( done ) {
1146+ // mock server on 6985 that errors for 1 request then returns the DB info block for other reqs
1147+ // and finally stops listening after 1s if it wasn't already closed
1148+ const mock = dataPhaseErrorMockServer ( 6985 , 1 , '{"doc_count":0}' , 1000 , done ) ;
1149+
1150+ // A client that will timeout after 100 ms and make only 2 attempts with a retry 10 ms after receiving an error
1151+ var cloudantClient = new Client ( {
1152+ https : false ,
1153+ maxAttempt : 2 ,
1154+ requestDefaults : { timeout : 100 } ,
1155+ plugins : { retry : { retryInitialDelayMsecs : 10 , retryStatusCodes : [ ] } }
1156+ } ) ;
1157+
1158+ var req = {
1159+ url : 'http://127.0.0.1:6985/foo' ,
1160+ auth : { username : ME , password : PASSWORD } ,
1161+ method : 'GET'
1162+ } ;
1163+
1164+ var responseCount = 0 ;
1165+ var errors = [ ] ;
1166+ var responseData = '' ;
1167+
1168+ cloudantClient . request ( req )
1169+ . on ( 'error' , ( err ) => {
1170+ errors . push ( err ) ;
1171+ } )
1172+ . on ( 'response' , function ( resp ) {
1173+ responseCount ++ ;
1174+ assert . equal ( resp . statusCode , 200 ) ;
1175+ } )
1176+ . on ( 'data' , function ( data ) {
1177+ responseData += data ;
1178+ } )
1179+ . on ( 'end' , function ( ) {
1180+ let expectedErrors = 1 ;
1181+ if ( process . version . startsWith ( 'v16.' ) ) {
1182+ // Node 16 has an additional `aborted` error
1183+ // https://github.com/nodejs/node/issues/28172
1184+ expectedErrors ++ ;
1185+ }
1186+ assert . ok ( responseData . toString ( 'utf8' ) . indexOf ( '"doc_count":0' ) > - 1 ) ;
1187+ assert . equal ( responseCount , 1 ) ;
1188+ assert . equal ( errors . length , expectedErrors ) ;
1189+ mock . close ( done ) ;
1190+ } ) ;
1191+ } ) ;
10871192 } ) ;
10881193
10891194 describe ( 'with callback and listener' , function ( ) {
@@ -1235,6 +1340,55 @@ describe('Retry Plugin', function() {
12351340 assert . fail ( 'Unexpected data from server' ) ;
12361341 } ) ;
12371342 } ) ;
1343+
1344+ it ( 'does not retry in response data phase' , function ( done ) {
1345+ // mock server on 6986 that errors for 1 request then returns the DB info block for other reqs
1346+ // and finally stops listening after 1s if it wasn't already closed
1347+ const mock = dataPhaseErrorMockServer ( 6986 , 1 , '{"doc_count":0}' , 1000 , done ) ;
1348+ // A client that will timeout after 100 ms and make only 2 attempts with a retry 10 ms after receiving an error
1349+ var cloudantClient = new Client ( {
1350+ https : false ,
1351+ maxAttempt : 2 ,
1352+ requestDefaults : { timeout : 100 } ,
1353+ plugins : { retry : { retryInitialDelayMsecs : 10 , retryStatusCodes : [ ] } }
1354+ } ) ;
1355+
1356+ var req = {
1357+ url : 'http://127.0.0.1:6986/foo' ,
1358+ auth : { username : ME , password : PASSWORD } ,
1359+ method : 'GET'
1360+ } ;
1361+
1362+ var responseCount = 0 ;
1363+ var errors = [ ] ;
1364+ var responseData = '' ;
1365+ cloudantClient . request ( req , function ( err , resp , data ) {
1366+ assert . ok ( err , 'Should get called back with an error.' ) ;
1367+ assert . equal ( 'ESOCKETTIMEDOUT' , err . code ) ;
1368+ mock . close ( ) ;
1369+ } )
1370+ . on ( 'error' , ( err ) => {
1371+ errors . push ( err ) ;
1372+ } )
1373+ . on ( 'response' , function ( resp ) {
1374+ responseCount ++ ;
1375+ assert . equal ( resp . statusCode , 200 ) ;
1376+ } )
1377+ . on ( 'data' , function ( data ) {
1378+ responseData += data ;
1379+ } )
1380+ . on ( 'end' , function ( ) {
1381+ let expectedErrors = 1 ;
1382+ if ( process . version . startsWith ( 'v16.' ) ) {
1383+ // Node 16 has an additional `aborted` error
1384+ // https://github.com/nodejs/node/issues/28172
1385+ expectedErrors ++ ;
1386+ }
1387+ assert . ok ( responseData . toString ( 'utf8' ) . indexOf ( '"doc_count":0' ) > - 1 ) ;
1388+ assert . equal ( responseCount , 1 ) ;
1389+ assert . equal ( errors . length , expectedErrors ) ;
1390+ } ) ;
1391+ } ) ;
12381392 } ) ;
12391393 } ) ;
12401394} ) ;
0 commit comments