diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index bba9994d7..582aec7c5 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -96,6 +96,7 @@ Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => { const XHR_LISTENER = zoneSymbol('xhrListener'); const XHR_SCHEDULED = zoneSymbol('xhrScheduled'); const XHR_URL = zoneSymbol('xhrURL'); + const XHR_ERROR_BEFORE_SCHEDULED = zoneSymbol('xhrErrorBeforeScheduled'); interface XHROptions extends TaskData { target: any; @@ -126,9 +127,10 @@ Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => { const SCHEDULED = 'scheduled'; function scheduleTask(task: Task) { - (XMLHttpRequest as any)[XHR_SCHEDULED] = false; const data = task.data; const target = data.target; + target[XHR_SCHEDULED] = false; + target[XHR_ERROR_BEFORE_SCHEDULED] = false; // remove existing event listener const listener = target[XHR_LISTENER]; if (!oriAddListener) { @@ -143,8 +145,11 @@ Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => { if (target.readyState === target.DONE) { // sometimes on some browsers XMLHttpRequest will fire onreadystatechange with // readyState=4 multiple times, so we need to check task state here - if (!data.aborted && (XMLHttpRequest as any)[XHR_SCHEDULED] && task.state === SCHEDULED) { + if (!data.aborted && target[XHR_SCHEDULED] && task.state === SCHEDULED) { task.invoke(); + } else if (!data.aborted && target[XHR_SCHEDULED] === false) { + // error occurs when xhr.send() + target[XHR_ERROR_BEFORE_SCHEDULED] = true; } } }; @@ -155,7 +160,7 @@ Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => { target[XHR_TASK] = task; } sendNative.apply(target, data.args); - (XMLHttpRequest as any)[XHR_SCHEDULED] = true; + target[XHR_SCHEDULED] = true; return task; } @@ -191,8 +196,15 @@ Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => { args: args, aborted: false }; - return scheduleMacroTaskWithCurrentZone( + const task = scheduleMacroTaskWithCurrentZone( XMLHTTPREQUEST_SOURCE, placeholderCallback, options, scheduleTask, clearTask); + if (self && self[XHR_ERROR_BEFORE_SCHEDULED] === true && !options.aborted && + task.state === SCHEDULED) { + // xhr request throw error when send + // we should invoke task instead of leaving a scheduled + // pending macroTask + task.invoke(); + } } }); diff --git a/test/browser/XMLHttpRequest.spec.ts b/test/browser/XMLHttpRequest.spec.ts index e6f9a705a..3a74420d6 100644 --- a/test/browser/XMLHttpRequest.spec.ts +++ b/test/browser/XMLHttpRequest.spec.ts @@ -251,6 +251,75 @@ describe('XMLHttpRequest', function() { }); }); + it('should trigger readystatechange if xhr request trigger cors error', (done) => { + const req = new XMLHttpRequest(); + let err: any = null; + try { + req.open('get', 'file:///test', true); + } catch (err) { + // in IE, open will throw Access is denied error + done(); + return; + } + req.addEventListener('readystatechange', function(ev) { + if (req.readyState === 4) { + const xhrScheduled = (req as any)['__zone_symbol__xhrScheduled']; + const task = (req as any)['__zone_symbol__xhrTask']; + if (xhrScheduled === false) { + expect(task.state).toEqual('scheduling'); + setTimeout(() => { + if (err) { + expect(task.state).toEqual('unknown'); + } else { + expect(task.state).toEqual('notScheduled'); + } + done(); + }); + } else { + expect(task.state).toEqual('scheduled'); + done(); + } + } + }); + try { + req.send(); + } catch (error) { + err = error; + } + }); + + it('should invoke task if xhr request trigger cors error', (done) => { + const logs: string[] = []; + const zone = Zone.current.fork({ + name: 'xhr', + onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => { + logs.push(JSON.stringify(hasTask)); + } + }); + const req = new XMLHttpRequest(); + try { + req.open('get', 'file:///test', true); + } catch (err) { + // in IE, open will throw Access is denied error + done(); + return; + } + zone.run(() => { + try { + (window as any)['__zone_symbol__setTimeout'](() => { + expect(logs).toEqual([ + `{"microTask":false,"macroTask":true,"eventTask":false,"change":"macroTask"}`, + `{"microTask":false,"macroTask":false,"eventTask":false,"change":"macroTask"}` + ]); + done(); + }, 500); + req.send(); + } catch (error) { + done(); + } + }); + }); + it('should not throw error when get XMLHttpRequest.prototype.onreadystatechange the first time', function() { const func = function() {