Skip to content
71 changes: 65 additions & 6 deletions handwritten/storage/src/resumable-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1386,9 +1386,7 @@ export class Upload extends Writable {

if (retryDelay <= 0) {
this.destroy(
new Error(
`Retry total time limit exceeded - ${JSON.stringify(resp.data)}`,
),
buildRetryError('Retry total time limit exceeded', resp),
);
return;
}
Expand All @@ -1409,9 +1407,7 @@ export class Upload extends Writable {
}
this.numRetries++;
} else {
this.destroy(
new Error(`Retry limit exceeded - ${JSON.stringify(resp.data)}`),
);
this.destroy(buildRetryError('Retry limit exceeded', resp));
}
}

Expand Down Expand Up @@ -1456,6 +1452,69 @@ export class Upload extends Writable {
}
}

function buildRetryError(
prefix: string,
resp: Pick<GaxiosResponse, 'data' | 'status'>,
): Error {
const parts: string[] = [];

if (typeof resp.status === 'number' && !isNaN(resp.status)) {
parts.push(`status: ${resp.status}`);
}

const err = resp.data;
if (err !== undefined && err !== null) {
if (typeof err === 'object') {
const gaxiosErrLike = err as any;
const errParts: string[] = [];
if (gaxiosErrLike.message) {
errParts.push(String(gaxiosErrLike.message));
}
const status = gaxiosErrLike.status ?? gaxiosErrLike.response?.status;
if (typeof status === 'number' && !isNaN(status) && status !== resp.status) {
errParts.push(`status: ${status}`);
}
const statusText = gaxiosErrLike.response?.statusText;
if (statusText) {
errParts.push(`statusText: ${statusText}`);
}
const responseData = gaxiosErrLike.response?.data;
if (responseData !== undefined && responseData !== null && responseData !== '') {
errParts.push(
`response: ${
typeof responseData === 'object'
? JSON.stringify(responseData)
: responseData
}`,
);
}
if (gaxiosErrLike.code) {
errParts.push(`code: ${String(gaxiosErrLike.code)}`);
}

if (errParts.length > 0) {
parts.push(...errParts);
} else if (err instanceof Error) {
parts.push(err.toString() || err.name || 'Unknown Error');
} else {
const stringified = JSON.stringify(err);
if (stringified && stringified !== '{}') {
parts.push(stringified);
}
}
} else if (typeof err === 'string') {
if (err !== '') {
parts.push(err);
}
} else {
parts.push(String(err));
}
}

const suffix = parts.join(' - ');
return new Error(`${prefix} - ${suffix || 'Unknown Error'}`);
}

export function upload(cfg: UploadConfig) {
return new Upload(cfg);
}
Expand Down
79 changes: 76 additions & 3 deletions handwritten/storage/test/resumable-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2287,7 +2287,7 @@ describe('resumable-upload', () => {
up.destroy = (err: Error) => {
assert.strictEqual(
err.message,
`Retry limit exceeded - ${JSON.stringify(RESP.data)}`
`Retry limit exceeded - status: 500 - error message from server`,
);
done();
};
Expand Down Expand Up @@ -2328,7 +2328,7 @@ describe('resumable-upload', () => {
assert.strictEqual(up.numRetries, 3);
assert.strictEqual(
err.message,
`Retry limit exceeded - ${JSON.stringify(RESP.data)}`
`Retry limit exceeded - status: 500 - error message from server`,
);
done();
});
Expand Down Expand Up @@ -2363,7 +2363,6 @@ describe('resumable-upload', () => {
return err.code === 1000;
};
up.retryOptions.retryableErrorFn = customHandlerFunction;

assert.strictEqual(up.onResponse(RESP), false);
});
});
Expand Down Expand Up @@ -2490,6 +2489,80 @@ describe('resumable-upload', () => {

up.attemptDelayedRetry({});
});

it('should include correct details for standard native Errors', done => {
up.numRetries = 3;
up.retryLimit = 3;
const nativeError = new Error('native connection issue');

up.on('error', (err: Error) => {
assert.strictEqual(
err.message,
'Retry limit exceeded - native connection issue',
);
done();
});

up.attemptDelayedRetry({
status: NaN,
data: nativeError,
});
});

it('should include correct details for custom errors with empty messages', done => {
up.numRetries = 3;
up.retryLimit = 3;
const customError = new Error('');
(customError as any).code = 'ERR_SOMETHING_SPECIAL';

up.on('error', (err: Error) => {
assert.strictEqual(
err.message,
'Retry limit exceeded - code: ERR_SOMETHING_SPECIAL',
);
done();
});

up.attemptDelayedRetry({
status: NaN,
data: customError,
});
});

it('should include correct details for GaxiosErrors with empty/missing response bodies', done => {
up.numRetries = 3;
up.retryLimit = 3;

const gaxiosError = new GaxiosError(
'Request failed with status code 429',
{
method: 'POST',
url: 'https://example.com',
} as any,
{
status: 429,
statusText: 'Too Many Requests',
data: '',
config: {},
headers: {},
} as any
);

up.on('error', (err: Error) => {
// Let's check for the presence of key details rather than an exact string if we are unsure of exact GaxiosError fields,
// or assert the expected string. Let's assert the expected string:
assert(err.message.includes('Retry limit exceeded'));
assert(err.message.includes('Request failed with status code 429'));
assert(err.message.includes('status: 429') || err.message.includes('code: 429'));
assert(err.message.includes('statusText: Too Many Requests'));
done();
});

up.attemptDelayedRetry({
status: NaN,
data: gaxiosError,
});
});
});

describe('PROTOCOL_REGEX', () => {
Expand Down
Loading