Skip to content

Commit c2657a8

Browse files
bgotinkbenlesh
authored andcommitted
fix(fromFetch): passing already aborted signal to init aborts fetch
* fix(fromFetch): copy init object instead of modifying it Modifying the object can lead to problems if the init object is reused in multiple calls to fromFetch. * fix(fromFetch): cancel fetch if init.signal is already aborted
1 parent 06fb735 commit c2657a8

File tree

2 files changed

+45
-11
lines changed

2 files changed

+45
-11
lines changed

spec/observables/dom/fetch-spec.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ function mockFetchImpl(input: string | Request, init?: RequestInit): Promise<Res
1010
(mockFetchImpl as MockFetch).calls.push({ input, init });
1111
return new Promise<any>((resolve, reject) => {
1212
if (init.signal) {
13+
if (init.signal.aborted) {
14+
reject(new MockDOMException());
15+
return;
16+
}
1317
init.signal.addEventListener('abort', () => {
1418
reject(new MockDOMException());
1519
});
1620
}
17-
return Promise.resolve(null).then(() => {
21+
Promise.resolve(null).then(() => {
1822
resolve((mockFetchImpl as any).respondWith);
1923
});
2024
});
@@ -173,13 +177,23 @@ describe('fromFetch', () => {
173177
});
174178

175179
it('should allow passing of init object', done => {
176-
const myInit = {};
177-
const fetch$ = fromFetch('/foo', myInit);
180+
const fetch$ = fromFetch('/foo', {method: 'HEAD'});
181+
fetch$.subscribe({
182+
error: done,
183+
complete: done,
184+
});
185+
expect(mockFetch.calls[0].init.method).to.equal('HEAD');
186+
});
187+
188+
it('should pass in a signal with the init object without mutating the init', done => {
189+
const myInit = {method: 'DELETE'};
190+
const fetch$ = fromFetch('/bar', myInit);
178191
fetch$.subscribe({
179192
error: done,
180193
complete: done,
181194
});
182-
expect(mockFetch.calls[0].init).to.equal(myInit);
195+
expect(mockFetch.calls[0].init.method).to.equal(myInit.method);
196+
expect(mockFetch.calls[0].init).not.to.equal(myInit);
183197
expect(mockFetch.calls[0].init.signal).not.to.be.undefined;
184198
});
185199

@@ -198,4 +212,20 @@ describe('fromFetch', () => {
198212
// The subscription will not be closed until the error fires when the promise resolves.
199213
expect(subscription.closed).to.be.false;
200214
});
215+
216+
it('should treat passed already aborted signals as a cancellation token which triggers an error', done => {
217+
const controller = new MockAbortController();
218+
controller.abort();
219+
const signal = controller.signal as any;
220+
const fetch$ = fromFetch('/foo', { signal });
221+
const subscription = fetch$.subscribe({
222+
error: err => {
223+
expect(err).to.be.instanceof(MockDOMException);
224+
done();
225+
}
226+
});
227+
expect(mockFetch.calls[0].init.signal.aborted).to.be.true;
228+
// The subscription will not be closed until the error fires when the promise resolves.
229+
expect(subscription.closed).to.be.false;
230+
});
201231
});

src/internal/observable/dom/fetch.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,18 @@ export function fromFetch(input: string | Request, init?: RequestInit): Observab
6161
if (init) {
6262
// If a signal is provided, just have it teardown. It's a cancellation token, basically.
6363
if (init.signal) {
64-
outerSignalHandler = () => {
65-
if (!signal.aborted) {
66-
controller.abort();
67-
}
68-
};
69-
init.signal.addEventListener('abort', outerSignalHandler);
64+
if (init.signal.aborted) {
65+
controller.abort();
66+
} else {
67+
outerSignalHandler = () => {
68+
if (!signal.aborted) {
69+
controller.abort();
70+
}
71+
};
72+
init.signal.addEventListener('abort', outerSignalHandler);
73+
}
7074
}
71-
init.signal = signal;
75+
init = { ...init, signal };
7276
} else {
7377
init = { signal };
7478
}

0 commit comments

Comments
 (0)