Skip to content

Commit a67eb5d

Browse files
authored
Fixed issue where pausing a resumable upload throws an error (#6938)
1 parent 0cd4000 commit a67eb5d

File tree

6 files changed

+186
-10
lines changed

6 files changed

+186
-10
lines changed

.changeset/eleven-cycles-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/storage": patch
3+
---
4+
5+
Fixed issue where pause throws an error when a request is in flight.

packages/storage/src/implementation/request.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ class NetworkRequest<I extends ConnectionType, O> implements Request<O> {
120120
const hitServer = connection.getErrorCode() === ErrorCode.NO_ERROR;
121121
const status = connection.getStatus();
122122
if (
123-
(!hitServer ||
124-
isRetryStatusCode(status, this.additionalRetryCodes_)) &&
125-
this.retry
123+
!hitServer ||
124+
(isRetryStatusCode(status, this.additionalRetryCodes_) &&
125+
this.retry)
126126
) {
127127
const wasCanceled = connection.getErrorCode() === ErrorCode.ABORT;
128128
backoffCallback(

packages/storage/test/integration/integration.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import { use, expect } from 'chai';
3737
import chaiAsPromised from 'chai-as-promised';
3838
import * as types from '../../src/public-types';
39+
import { Deferred } from '@firebase/util';
3940

4041
use(chaiAsPromised);
4142

@@ -187,4 +188,32 @@ describe('FirebaseStorage Exp', () => {
187188
expect(listResult.items.map(v => v.name)).to.have.members(['a', 'b']);
188189
expect(listResult.prefixes.map(v => v.name)).to.have.members(['c']);
189190
});
191+
192+
it('can pause uploads without an error', async () => {
193+
const referenceA = ref(storage, 'public/exp-upload/a');
194+
const bytesToUpload = new ArrayBuffer(1024 * 1024);
195+
const task = uploadBytesResumable(referenceA, bytesToUpload);
196+
const failureDeferred = new Deferred();
197+
let hasPaused = false;
198+
task.on(
199+
'state_changed',
200+
snapshot => {
201+
if (snapshot.bytesTransferred > 0 && !hasPaused) {
202+
task.pause();
203+
hasPaused = true;
204+
}
205+
},
206+
() => {
207+
failureDeferred.reject('Failed to upload file');
208+
}
209+
);
210+
await Promise.race([
211+
failureDeferred.promise,
212+
new Promise(resolve => setTimeout(resolve, 4000))
213+
]);
214+
task.resume();
215+
await task;
216+
const bytes = await getBytes(referenceA);
217+
expect(bytes).to.deep.eq(bytesToUpload);
218+
});
190219
});

packages/storage/test/unit/connection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export class TestingConnection implements Connection<string> {
117117

118118
abort(): void {
119119
this.state = State.START;
120-
this.errorCode = ErrorCode.NO_ERROR;
120+
this.errorCode = ErrorCode.ABORT;
121121
this.resolve();
122122
}
123123

packages/storage/test/unit/task.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,142 @@ describe('Firebase Storage > Upload Task', () => {
341341
});
342342

343343
task.pause();
344+
return promise;
345+
}
346+
347+
// This is to test to make sure that when you pause in the middle of a request that you do not get an error
348+
async function runProgressPauseTest(blob: FbsBlob): Promise<void> {
349+
let callbackCount = 0;
350+
// Usually the first request is to create the resumable upload and the second is to upload.
351+
// Upload requests are not retryable, and this callback is to make sure we pause before the response comes back.
352+
function shouldRespondCallback(): boolean {
353+
if (callbackCount++ === 1) {
354+
task.pause();
355+
return false;
356+
}
357+
return true;
358+
}
359+
const storageService = storageServiceWithHandler(
360+
fakeServerHandler(),
361+
shouldRespondCallback
362+
);
363+
const task = new UploadTask(
364+
new Reference(storageService, testLocation),
365+
blob
366+
);
367+
368+
// eslint-disable-next-line @typescript-eslint/ban-types
369+
let resolve: Function, reject: Function;
370+
const promise = new Promise<void>((innerResolve, innerReject) => {
371+
resolve = innerResolve;
372+
reject = innerReject;
373+
});
374+
375+
// Assert functions throw Errors, which means if we're not in the right stack
376+
// the error might not get reported properly. This function wraps existing
377+
// assert functions, returning a new function that calls reject with any
378+
// caught errors. This should make sure errors are reported properly.
379+
function promiseAssertWrapper<T>(func: T): T {
380+
function wrapped(..._args: any[]): void {
381+
try {
382+
(func as any as (...args: any[]) => void).apply(null, _args);
383+
} catch (e) {
384+
reject(e);
385+
pausedStateCompleted.reject(e);
386+
// also throw to further unwind the stack
387+
throw e;
388+
}
389+
}
390+
return wrapped as any as T;
391+
}
392+
393+
const fixedAssertEquals = promiseAssertWrapper(assert.equal);
394+
const fixedAssertFalse = promiseAssertWrapper(assert.isFalse);
395+
const fixedAssertTrue = promiseAssertWrapper(assert.isTrue);
396+
const fixedAssertFail = promiseAssertWrapper(assert.fail);
397+
398+
const events: string[] = [];
399+
const progress: number[][] = [];
400+
// Promise for when we are finally in the pause state
401+
const pausedStateCompleted = new Deferred();
402+
let complete = 0;
403+
// Adds a callback for when the state has changed. The callback resolves the pausedStateCompleted promise
404+
// to let our test know when to resume.
405+
function addCallbacks(task: UploadTask): void {
406+
let lastState: string;
407+
task.on(
408+
TaskEvent.STATE_CHANGED,
409+
snapshot => {
410+
fixedAssertEquals(complete, 0);
411+
412+
const state = snapshot.state;
413+
if (lastState !== TaskState.RUNNING && state === TaskState.RUNNING) {
414+
events.push('resume');
415+
} else if (
416+
lastState !== TaskState.PAUSED &&
417+
state === TaskState.PAUSED
418+
) {
419+
pausedStateCompleted.resolve();
420+
events.push('pause');
421+
}
422+
423+
const p = [snapshot.bytesTransferred, snapshot.totalBytes];
424+
425+
progress.push(p);
426+
427+
lastState = state;
428+
},
429+
() => {
430+
fixedAssertFail('Failed to upload');
431+
},
432+
() => {
433+
events.push('complete');
434+
complete++;
435+
}
436+
);
437+
}
438+
439+
addCallbacks(task);
344440

441+
let completeTriggered = false;
442+
443+
// We should clean this up and just add all callbacks in one function call.
444+
// Keeps track of all events that were logged before and asserts on them.
445+
task.on(TaskEvent.STATE_CHANGED, undefined, undefined, () => {
446+
fixedAssertFalse(completeTriggered);
447+
completeTriggered = true;
448+
449+
fixedAssertEquals(events.length, 4);
450+
fixedAssertEquals(events[0], 'resume');
451+
fixedAssertEquals(events[1], 'pause');
452+
fixedAssertEquals(events[2], 'resume');
453+
fixedAssertEquals(events[3], 'complete');
454+
455+
fixedAssertEquals(complete, 1);
456+
457+
let increasing = true;
458+
let allTotalsTheSame = true;
459+
for (let i = 0; i < progress.length - 1; i++) {
460+
increasing = increasing && progress[i][0] <= progress[i + 1][0];
461+
allTotalsTheSame =
462+
allTotalsTheSame && progress[i][1] === progress[i + 1][1];
463+
}
464+
465+
let lastIsAll = false;
466+
if (progress.length > 0) {
467+
const last = progress[progress.length - 1];
468+
lastIsAll = last[0] === last[1];
469+
} else {
470+
lastIsAll = true;
471+
}
472+
473+
fixedAssertTrue(increasing);
474+
fixedAssertTrue(allTotalsTheSame);
475+
fixedAssertTrue(lastIsAll);
476+
resolve(null);
477+
});
478+
await pausedStateCompleted.promise;
479+
task.resume();
345480
return promise;
346481
}
347482
enum StateType {
@@ -593,6 +728,10 @@ describe('Firebase Storage > Upload Task', () => {
593728
expect(clock.countTimers()).to.eq(0);
594729
clock.restore();
595730
});
731+
it('does not error when pausing inflight request', async () => {
732+
// Kick off upload
733+
await runProgressPauseTest(bigBlob);
734+
});
596735
it('tests if small requests that respond with 500 retry correctly', async () => {
597736
clock = useFakeTimers();
598737
// Kick off upload

packages/storage/test/unit/testshared.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,8 @@ export type RequestHandler = (
205205
) => Response;
206206

207207
export function storageServiceWithHandler(
208-
handler: RequestHandler
208+
handler: RequestHandler,
209+
shouldResponseCb?: () => boolean
209210
): FirebaseStorageImpl {
210211
function newSend(
211212
connection: TestingConnection,
@@ -215,11 +216,13 @@ export function storageServiceWithHandler(
215216
headers?: Headers
216217
): void {
217218
const response = handler(url, method, body, headers);
218-
connection.simulateResponse(
219-
response.status,
220-
response.body,
221-
response.headers
222-
);
219+
if (!shouldResponseCb || shouldResponseCb()) {
220+
connection.simulateResponse(
221+
response.status,
222+
response.body,
223+
response.headers
224+
);
225+
}
223226
}
224227

225228
injectTestConnection(() => newTestConnection(newSend));

0 commit comments

Comments
 (0)