Skip to content

Commit 3c0acdd

Browse files
committed
feat: support retrying requests when rate limited
1 parent 10392d5 commit 3c0acdd

File tree

2 files changed

+57
-20
lines changed

2 files changed

+57
-20
lines changed

docs/classes/google-spreadsheet.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ _Class Reference_
99
## Initialization
1010

1111
### Existing documents
12-
#### `new GoogleSpreadsheet(id, auth)` :id=fn-newGoogleSpreadsheet
12+
#### `new GoogleSpreadsheet(id, auth, rateLimitedRetryConfig)` :id=fn-newGoogleSpreadsheet
1313
> Work with an existing document
1414
1515
> You'll need the document ID, which you can find in your browser's URL when you navigate to the document.<br/>
@@ -19,7 +19,7 @@ Param|Type|Required|Description
1919
---|---|---|---
2020
`spreadsheetId` | String | ✅ | Document ID
2121
`auth` | `GoogleAuth` \|<br/> `JWT` \|<br/> `OAuth2Client` \|<br/> `{ apiKey: string }` \|<br/> `{ token: string }` | ✅ | Authentication to use<br/>See [Authentication](guides/authentication) for more info
22-
22+
`rateLimitedRetryConfig` | `{` <br/>`maxRetries: number,`<br/>`retryStrategy: (retryCount: number) => number`<br/> `}` | ❎ | Configure handling for rate limited responses
2323

2424
### Creating a new document
2525

src/lib/GoogleSpreadsheet.ts

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ const EXPORT_CONFIG: Record<string, { singleWorksheet?: boolean }> = {
2828
};
2929
type ExportFileTypes = keyof typeof EXPORT_CONFIG;
3030

31-
32-
31+
type RateLimitedRetryConfig = {
32+
/**
33+
* The maximum number of times to retry the request. 0 means no retries.
34+
*/
35+
maxRetries: number,
36+
/**
37+
* A function that takes the current retry count and returns the number of milliseconds to wait before the next retry.
38+
* @see https://developers.google.com/sheets/api/limits#example-algorithm
39+
*/
40+
retryStrategy: (retryCount: number) => number,
41+
};
3342

3443
function getAuthMode(auth: GoogleApiAuth) {
3544
if ('getRequestHeaders' in auth) return AUTH_MODES.GOOGLE_AUTH_CLIENT;
@@ -81,6 +90,7 @@ export class GoogleSpreadsheet {
8190
private _rawProperties = null as SpreadsheetProperties | null;
8291
private _spreadsheetUrl = null as string | null;
8392
private _deleted = false;
93+
private _rateLimitedRetryConfig: RateLimitedRetryConfig | undefined;
8494

8595
/**
8696
* Sheets API [axios](https://axios-http.com) instance
@@ -108,7 +118,8 @@ export class GoogleSpreadsheet {
108118
/** id of google spreadsheet doc */
109119
spreadsheetId: SpreadsheetId,
110120
/** authentication to use with Google Sheets API */
111-
auth: GoogleApiAuth
121+
auth: GoogleApiAuth,
122+
rateLimitedRetryConfig?: RateLimitedRetryConfig
112123
) {
113124
this.spreadsheetId = spreadsheetId;
114125
this.auth = auth;
@@ -140,8 +151,11 @@ export class GoogleSpreadsheet {
140151
this._handleAxiosResponse.bind(this),
141152
this._handleAxiosErrors.bind(this)
142153
);
143-
}
144154

155+
if (rateLimitedRetryConfig) {
156+
this._rateLimitedRetryConfig = rateLimitedRetryConfig;
157+
}
158+
}
145159

146160
// AUTH RELATED FUNCTIONS ////////////////////////////////////////////////////////////////////////
147161

@@ -160,25 +174,48 @@ export class GoogleSpreadsheet {
160174
/** @internal */
161175
async _handleAxiosResponse(response: AxiosResponse) { return response; }
162176
/** @internal */
163-
async _handleAxiosErrors(error: AxiosError) {
164-
// console.log(error);
165-
const errorData = error.response?.data as any;
177+
async _handleAxiosErrors(axiosInstance: AxiosInstance) {
178+
return (error: AxiosError) => {
179+
const responseStatusCode = _.get(error, 'response.status');
180+
181+
// Handle rate limited responses by retrying based on the rate limited retry config
182+
if (responseStatusCode === 429) {
183+
const config = error.config as InternalAxiosRequestConfig & { retryCount?: number };
184+
const retryCount = config?.retryCount ?? 0;
185+
186+
const rateLimitedRetryConfig = this._rateLimitedRetryConfig;
187+
if (rateLimitedRetryConfig && retryCount < rateLimitedRetryConfig.maxRetries) {
188+
config.retryCount = retryCount + 1;
189+
const backoff = rateLimitedRetryConfig.retryStrategy(retryCount);
190+
return new Promise((resolve) => {
191+
setTimeout(
192+
() => {
193+
resolve(axiosInstance(config));
194+
},
195+
backoff
196+
);
197+
});
198+
}
199+
}
200+
// console.log(error);
201+
const errorData = error.response?.data as any;
166202

167-
if (errorData) {
203+
if (errorData) {
168204
// usually the error has a code and message, but occasionally not
169-
if (!errorData.error) throw error;
205+
if (!errorData.error) throw error;
170206

171-
const { code, message } = errorData.error;
172-
error.message = `Google API error - [${code}] ${message}`;
173-
throw error;
174-
}
207+
const { code, message } = errorData.error;
208+
error.message = `Google API error - [${code}] ${message}`;
209+
throw error;
210+
}
175211

176-
if (_.get(error, 'response.status') === 403) {
177-
if ('apiKey' in this.auth) {
178-
throw new Error('Sheet is private. Use authentication or make public. (see https://github.com/theoephraim/node-google-spreadsheet#a-note-on-authentication for details)');
212+
if (responseStatusCode === 403) {
213+
if ('apiKey' in this.auth) {
214+
throw new Error('Sheet is private. Use authentication or make public. (see https://github.com/theoephraim/node-google-spreadsheet#a-note-on-authentication for details)');
215+
}
179216
}
180-
}
181-
throw error;
217+
throw error;
218+
};
182219
}
183220

184221
/** @internal */

0 commit comments

Comments
 (0)