@@ -223,4 +223,149 @@ describe('EventSource', () => {
223223
224224 expect ( onopen ) . toHaveBeenCalledTimes ( 1 ) ;
225225 } ) ;
226+
227+ test ( 'calls retryAndHandleError with parsed response headers on an error response' , ( ) => {
228+ const retryAndHandleError = jest . fn ( ( ) => false ) ;
229+
230+ mockXhr . getAllResponseHeaders = jest . fn (
231+ ( ) => 'X-Ld-Fd-Fallback: true\r\nX-Ld-Fd-Fallback-Ttl: 60\r\nContent-Type: text/event-stream' ,
232+ ) ;
233+ mockXhr . responseText = 'error body' ;
234+
235+ const es = new EventSource < EventName > ( uri , { logger, retryAndHandleError } ) ;
236+ es . onerror = jest . fn ( ) ;
237+
238+ jest . runAllTimers ( ) ;
239+
240+ mockXhr . readyState = 4 ;
241+ mockXhr . status = 500 ;
242+ mockXhr . onreadystatechange ( ) ;
243+
244+ expect ( retryAndHandleError ) . toHaveBeenCalledTimes ( 1 ) ;
245+ expect ( retryAndHandleError ) . toHaveBeenCalledWith (
246+ expect . objectContaining ( {
247+ status : 500 ,
248+ message : 'error body' ,
249+ headers : expect . objectContaining ( {
250+ 'x-ld-fd-fallback' : 'true' ,
251+ 'x-ld-fd-fallback-ttl' : '60' ,
252+ } ) ,
253+ } ) ,
254+ ) ;
255+ } ) ;
256+
257+ test ( 'dispatches error with status and headers from onprogress mid-stream' , ( ) => {
258+ const onerror = jest . fn ( ) ;
259+
260+ mockXhr . getAllResponseHeaders = jest . fn (
261+ ( ) => 'X-Ld-Fd-Fallback: true\r\nX-Ld-Fd-Fallback-Ttl: 60\r\nContent-Type: text/event-stream' ,
262+ ) ;
263+ mockXhr . responseText = 'error body' ;
264+
265+ eventSource . onerror = onerror ;
266+
267+ jest . runAllTimers ( ) ;
268+
269+ // readyState 3 (LOADING), not DONE, so this exercises onprogress's
270+ // error branch rather than onreadystatechange's.
271+ mockXhr . readyState = 3 ;
272+ mockXhr . status = 500 ;
273+ mockXhr . onprogress ( ) ;
274+
275+ expect ( onerror ) . toHaveBeenCalledWith (
276+ expect . objectContaining ( {
277+ status : 500 ,
278+ headers : expect . objectContaining ( {
279+ 'x-ld-fd-fallback' : 'true' ,
280+ 'x-ld-fd-fallback-ttl' : '60' ,
281+ } ) ,
282+ } ) ,
283+ ) ;
284+ } ) ;
285+
286+ test ( 'dispatches error with status and headers from onreadystatechange before retrying' , ( ) => {
287+ const onerror = jest . fn ( ) ;
288+
289+ mockXhr . getAllResponseHeaders = jest . fn (
290+ ( ) => 'X-Ld-Fd-Fallback: true\r\nX-Ld-Fd-Fallback-Ttl: 60\r\nContent-Type: text/event-stream' ,
291+ ) ;
292+ mockXhr . responseText = 'error body' ;
293+
294+ eventSource . onerror = onerror ;
295+
296+ jest . runAllTimers ( ) ;
297+
298+ mockXhr . readyState = 4 ;
299+ mockXhr . status = 500 ;
300+ mockXhr . onreadystatechange ( ) ;
301+
302+ expect ( onerror ) . toHaveBeenCalledWith (
303+ expect . objectContaining ( {
304+ status : 500 ,
305+ headers : expect . objectContaining ( {
306+ 'x-ld-fd-fallback' : 'true' ,
307+ 'x-ld-fd-fallback-ttl' : '60' ,
308+ } ) ,
309+ } ) ,
310+ ) ;
311+ } ) ;
312+
313+ test ( 'invokes retryAndHandleError from onprogress when the connection never reaches DONE' , ( ) => {
314+ const retryAndHandleError = jest . fn ( ( ) => false ) ;
315+
316+ mockXhr . getAllResponseHeaders = jest . fn (
317+ ( ) => 'X-Ld-Fd-Fallback: true\r\nX-Ld-Fd-Fallback-Ttl: 60\r\nContent-Type: text/event-stream' ,
318+ ) ;
319+ mockXhr . responseText = 'error body' ;
320+
321+ const es = new EventSource < EventName > ( uri , { logger, retryAndHandleError } ) ;
322+ es . onerror = jest . fn ( ) ;
323+
324+ jest . runAllTimers ( ) ;
325+
326+ // readyState never advances to DONE for this attempt, so
327+ // onreadystatechange never fires; onprogress must drive the retry
328+ // on its own.
329+ mockXhr . readyState = 3 ;
330+ mockXhr . status = 500 ;
331+ mockXhr . onprogress ( ) ;
332+
333+ expect ( retryAndHandleError ) . toHaveBeenCalledTimes ( 1 ) ;
334+ expect ( retryAndHandleError ) . toHaveBeenCalledWith (
335+ expect . objectContaining ( {
336+ status : 500 ,
337+ message : 'error body' ,
338+ headers : expect . objectContaining ( {
339+ 'x-ld-fd-fallback' : 'true' ,
340+ 'x-ld-fd-fallback-ttl' : '60' ,
341+ } ) ,
342+ } ) ,
343+ ) ;
344+ } ) ;
345+
346+ test ( 'does not invoke retryAndHandleError twice when onprogress and onreadystatechange observe the same failed attempt' , ( ) => {
347+ const retryAndHandleError = jest . fn ( ( ) => false ) ;
348+
349+ mockXhr . getAllResponseHeaders = jest . fn (
350+ ( ) => 'X-Ld-Fd-Fallback: true\r\nX-Ld-Fd-Fallback-Ttl: 60\r\nContent-Type: text/event-stream' ,
351+ ) ;
352+ mockXhr . responseText = 'error body' ;
353+
354+ const es = new EventSource < EventName > ( uri , { logger, retryAndHandleError } ) ;
355+ es . onerror = jest . fn ( ) ;
356+
357+ jest . runAllTimers ( ) ;
358+
359+ // onprogress observes the error first (LOADING), then the same response
360+ // reaches DONE via onreadystatechange. retryAndHandleError must fire
361+ // exactly once for this attempt despite both handlers seeing the error.
362+ mockXhr . readyState = 3 ;
363+ mockXhr . status = 500 ;
364+ mockXhr . onprogress ( ) ;
365+
366+ mockXhr . readyState = 4 ;
367+ mockXhr . onreadystatechange ( ) ;
368+
369+ expect ( retryAndHandleError ) . toHaveBeenCalledTimes ( 1 ) ;
370+ } ) ;
226371} ) ;
0 commit comments