Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions src/resumable-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,9 +742,18 @@ export class Upload extends Writable {
responseReceived = true;
this.responseHandler(resp);
}
} catch (err) {
const e = err as Error;
this.destroy(e);
} catch (e) {
const err = e as ApiError;

if (this.retryOptions.retryableErrorFn!(err)) {
this.attemptDelayedRetry({
status: NaN,
data: err,
});
return;
}

this.destroy(err);
}
}

Expand Down Expand Up @@ -833,7 +842,16 @@ export class Upload extends Writable {
}
this.offset = 0;
} catch (e) {
const err = e as GaxiosError;
const err = e as ApiError;

if (this.retryOptions.retryableErrorFn!(err)) {
this.attemptDelayedRetry({
status: NaN,
data: err,
});
return;
}

this.destroy(err);
}
}
Expand Down Expand Up @@ -941,7 +959,7 @@ export class Upload extends Writable {
/**
* @param resp GaxiosResponse object from previous attempt
*/
private attemptDelayedRetry(resp: GaxiosResponse) {
private attemptDelayedRetry(resp: Pick<GaxiosResponse, 'data' | 'status'>) {
if (this.numRetries < this.retryOptions.maxRetries!) {
if (
resp.status === NOT_FOUND_STATUS_CODE &&
Expand Down
9 changes: 5 additions & 4 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,12 @@ const IDEMPOTENCY_STRATEGY_DEFAULT = IdempotencyStrategy.RetryConditional;
* @return {boolean} True if the API request should be retried, false otherwise.
*/
export const RETRYABLE_ERR_FN_DEFAULT = function (err?: ApiError) {
const isConnectionProblem = (reason: string | undefined) => {
const isConnectionProblem = (reason: string) => {
return (
(reason && reason.includes('eai_again')) || //DNS lookup error
reason.includes('eai_again') || // DNS lookup error
reason === 'econnreset' ||
reason === 'unexpected connection closure'
reason === 'unexpected connection closure' ||
reason === 'epipe'
);
};

Expand All @@ -284,7 +285,7 @@ export const RETRYABLE_ERR_FN_DEFAULT = function (err?: ApiError) {
if (err.errors) {
for (const e of err.errors) {
const reason = e?.reason?.toString().toLowerCase();
if (isConnectionProblem(reason)) {
if (reason && isConnectionProblem(reason)) {
return true;
}
}
Expand Down
14 changes: 14 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,20 @@ describe('Storage', () => {
assert.strictEqual(calledWith.retryOptions.retryableErrorFn(error), true);
});

it('should retry a broken pipe error', () => {
const storage = new Storage({
projectId: PROJECT_ID,
});
const calledWith = storage.calledWith_[0];
const error = new ApiError('Broken pipe');
error.errors = [
{
reason: 'EPIPE',
},
];
assert.strictEqual(calledWith.retryOptions.retryableErrorFn(error), true);
});

it('should not retry a 999 error', () => {
const storage = new Storage({
projectId: PROJECT_ID,
Expand Down
34 changes: 33 additions & 1 deletion test/resumable-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('resumable-upload', () => {
userProject: USER_PROJECT,
authConfig: {keyFile},
apiEndpoint: API_ENDPOINT,
retryOptions: RETRY_OPTIONS,
retryOptions: {...RETRY_OPTIONS},
});
});

Expand Down Expand Up @@ -1028,6 +1028,22 @@ describe('resumable-upload', () => {
up.startUploading();
});

it('should retry retryable errors if the request failed', done => {
const error = new Error('Error.');

// mock as retryable
up.retryOptions.retryableErrorFn = () => true;

up.on('error', done);
up.attemptDelayedRetry = () => done();

up.makeRequestStream = async () => {
throw error;
};

up.startUploading();
});

describe('request preparation', () => {
// a convenient handle for getting the request options
let reqOpts: GaxiosOptions;
Expand Down Expand Up @@ -1384,6 +1400,22 @@ describe('resumable-upload', () => {
await up.getAndSetOffset();
assert.strictEqual(up.offset, 0);
});

it('should retry retryable errors if the request failed', done => {
const error = new Error('Error.');

// mock as retryable
up.retryOptions.retryableErrorFn = () => true;

up.on('error', done);
up.attemptDelayedRetry = () => done();

up.makeRequest = async () => {
throw error;
};

up.getAndSetOffset();
});
});

describe('#makeRequest', () => {
Expand Down