Skip to content

Commit c65ba4d

Browse files
committed
feat: add retry config customization in app init
1 parent ba2bb67 commit c65ba4d

13 files changed

+94
-16
lines changed

etc/firebase-admin.api.md

+11
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ export interface AppOptions {
7272
credential?: Credential;
7373
databaseAuthVariableOverride?: object | null;
7474
databaseURL?: string;
75+
disableRetry?: boolean;
7576
httpAgent?: Agent;
7677
projectId?: string;
78+
retryConfig?: RetryConfig;
7779
serviceAccountId?: string;
7880
storageBucket?: string;
7981
}
@@ -462,6 +464,15 @@ export namespace remoteConfig {
462464
export type Version = Version;
463465
}
464466

467+
// @public
468+
export interface RetryConfig {
469+
backOffFactor?: number;
470+
ioErrorCodes?: string[];
471+
maxDelayInMillis: number;
472+
maxRetries: number;
473+
statusCodes?: number[];
474+
}
475+
465476
// @public (undocumented)
466477
export const SDK_VERSION: string;
467478

etc/firebase-admin.app.api.md

+11
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ export interface AppOptions {
2222
credential?: Credential;
2323
databaseAuthVariableOverride?: object | null;
2424
databaseURL?: string;
25+
disableRetry?: boolean;
2526
httpAgent?: Agent;
2627
projectId?: string;
28+
retryConfig?: RetryConfig;
2729
serviceAccountId?: string;
2830
storageBucket?: string;
2931
}
@@ -73,6 +75,15 @@ export function initializeApp(options?: AppOptions, appName?: string): App;
7375
// @public
7476
export function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential;
7577

78+
// @public
79+
export interface RetryConfig {
80+
backOffFactor?: number;
81+
ioErrorCodes?: string[];
82+
maxDelayInMillis: number;
83+
maxRetries: number;
84+
statusCodes?: number[];
85+
}
86+
7687
// @public (undocumented)
7788
export const SDK_VERSION: string;
7889

src/app/core.ts

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import { Agent } from 'http';
1919

2020
import { Credential } from './credential';
21+
import { RetryConfig } from '.'
2122

2223
/**
2324
* Available options to pass to {@link firebase-admin.app#initializeApp}.
@@ -83,6 +84,16 @@ export interface AppOptions {
8384
* specifying an HTTP Agent in the corresponding factory methods.
8485
*/
8586
httpAgent?: Agent;
87+
88+
/**
89+
* The retry configuration that will be used in HttpClient for API Requests.
90+
*/
91+
retryConfig?: RetryConfig;
92+
93+
/**
94+
* Completely disable the retry strategy in HttpClient.
95+
*/
96+
disableRetry?: boolean;
8697
}
8798

8899
/**

src/app/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ export { initializeApp, getApp, getApps, deleteApp } from './lifecycle';
2828

2929
export { Credential, ServiceAccount, GoogleOAuthAccessToken } from './credential';
3030
export { applicationDefault, cert, refreshToken } from './credential-factory';
31+
export { RetryConfig } from '../utils/api-request'
3132

3233
export const SDK_VERSION = getSdkVersion();

src/auth/auth-api-request.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const FIREBASE_AUTH_HEADER = {
4848
'X-Client-Version': `Node/Admin/${utils.getSdkVersion()}`,
4949
};
5050
/** Firebase Auth request timeout duration in milliseconds. */
51-
const FIREBASE_AUTH_TIMEOUT = 25000;
51+
const FIREBASE_AUTH_TIMEOUT = parseInt(process.env.FIREBASE_AUTH_TIMEOUT || '25000', 10);
5252

5353

5454
/** List of reserved claims which cannot be provided when creating a custom token. */

src/firebase-namespace-api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export { projectManagement } from './project-management/project-management-names
9191
export { remoteConfig } from './remote-config/remote-config-namespace';
9292
export { securityRules } from './security-rules/security-rules-namespace';
9393
export { storage } from './storage/storage-namespace';
94+
export { RetryConfig } from './utils/api-request'
9495

9596
// Declare other top-level members of the admin namespace below. Unfortunately, there's no
9697
// compile-time mechanism to ensure that the FirebaseNamespace class actually provides these

src/messaging/batch-request-internal.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import { FirebaseAppError, AppErrorCodes } from '../utils/error';
2121

2222
const PART_BOUNDARY = '__END_OF_PART__';
23-
const TEN_SECONDS_IN_MILLIS = 10000;
23+
const FIREBASE_MESSAGING_BATCH_TIMEOUT = parseInt(process.env.FIREBASE_MESSAGING_BATCH_TIMEOUT || '10000', 10);
2424

2525
/**
2626
* Represents a request that can be sent as part of an HTTP batch request.
@@ -72,7 +72,7 @@ export class BatchRequestClient {
7272
url: this.batchUrl,
7373
data: this.getMultipartPayload(requests),
7474
headers: Object.assign({}, this.commonHeaders, requestHeaders),
75-
timeout: TEN_SECONDS_IN_MILLIS,
75+
timeout: FIREBASE_MESSAGING_BATCH_TIMEOUT,
7676
};
7777
return this.httpClient.send(request).then((response) => {
7878
if (!response.multipart) {

src/messaging/messaging-api-request-internal.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { SendResponse, BatchResponse } from './messaging-api';
2727

2828

2929
// FCM backend constants
30-
const FIREBASE_MESSAGING_TIMEOUT = 10000;
30+
const FIREBASE_MESSAGING_TIMEOUT = parseInt(process.env.FIREBASE_MESSAGING_TIMEOUT || '10000', 10);
3131
const FIREBASE_MESSAGING_BATCH_URL = 'https://fcm.googleapis.com/batch';
3232
const FIREBASE_MESSAGING_HTTP_METHOD: HttpMethod = 'POST';
3333
const FIREBASE_MESSAGING_HEADERS = {

src/project-management/project-management-api-request-internal.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const PROJECT_MANAGEMENT_HEADERS = {
3636
'X-Client-Version': `Node/Admin/${getSdkVersion()}`,
3737
};
3838
/** Project management request timeout duration in milliseconds. */
39-
const PROJECT_MANAGEMENT_TIMEOUT_MILLIS = 10000;
39+
const FIREBASE_PROJECT_MANAGEMENT_TIMEOUT = parseInt(process.env.FIREBASE_PROJECT_MANAGEMENT_TIMEOUT || '10000', 10);
4040

4141
const LIST_APPS_MAX_PAGE_SIZE = 100;
4242

@@ -315,7 +315,7 @@ export class ProjectManagementRequestHandler {
315315
url: `${baseUrlToUse}${path}`,
316316
headers: PROJECT_MANAGEMENT_HEADERS,
317317
data: requestData,
318-
timeout: PROJECT_MANAGEMENT_TIMEOUT_MILLIS,
318+
timeout: FIREBASE_PROJECT_MANAGEMENT_TIMEOUT,
319319
};
320320

321321
return this.httpClient.send(request)

src/utils/api-request.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,13 @@ function validateRetryConfig(retry: RetryConfig): void {
243243

244244
export class HttpClient {
245245

246-
constructor(private readonly retry: RetryConfig | null = defaultRetryConfig()) {
247-
if (this.retry) {
246+
constructor(
247+
private readonly retry: RetryConfig | null = null,
248+
private readonly disableRetry: boolean = false
249+
) {
250+
if (!this.disableRetry && !this.retry) {
251+
this.retry = defaultRetryConfig();
252+
} else if (!this.disableRetry && this.retry) {
248253
validateRetryConfig(this.retry);
249254
}
250255
}
@@ -345,7 +350,7 @@ export class HttpClient {
345350
}
346351

347352
private isRetryEligible(retryAttempts: number, err: LowLevelError): boolean {
348-
if (!this.retry) {
353+
if (this.disableRetry || !this.retry) {
349354
return false;
350355
}
351356

@@ -811,7 +816,7 @@ class HttpRequestConfigImpl implements HttpRequestConfig {
811816

812817
export class AuthorizedHttpClient extends HttpClient {
813818
constructor(private readonly app: FirebaseApp) {
814-
super();
819+
super(app.options.retryConfig, app.options.disableRetry);
815820
}
816821

817822
public send(request: HttpRequestConfig): Promise<HttpResponse> {

test/resources/mocks.ts

+12
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,19 @@ export const databaseAuthVariableOverride = { 'some#string': 'some#val' };
5353
export const storageBucket = 'bucketName.appspot.com';
5454

5555
export const credential = cert(path.resolve(__dirname, './mock.key.json'));
56+
export const retryConfig = {
57+
maxRetries: 15,
58+
statusCodes: [507],
59+
ioErrorCodes: ['ECONNRESET', 'ETIMEDOUT'],
60+
backOffFactor: 0.8,
61+
maxDelayInMillis: 190000,
62+
}
5663

5764
export const appOptions: AppOptions = {
5865
credential,
5966
databaseURL,
6067
storageBucket,
68+
retryConfig,
6169
};
6270

6371
export const appOptionsWithOverride: AppOptions = {
@@ -66,19 +74,23 @@ export const appOptionsWithOverride: AppOptions = {
6674
databaseURL,
6775
storageBucket,
6876
projectId,
77+
retryConfig,
6978
};
7079

7180
export const appOptionsNoAuth: AppOptions = {
7281
databaseURL,
82+
retryConfig,
7383
};
7484

7585
export const appOptionsNoDatabaseUrl: AppOptions = {
7686
credential,
87+
retryConfig,
7788
};
7889

7990
export const appOptionsAuthDB: AppOptions = {
8091
credential,
8192
databaseURL,
93+
retryConfig,
8294
};
8395

8496
export class MockCredential implements Credential {

test/unit/machine-learning/machine-learning.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ describe('MachineLearning', () => {
582582
stubs.push(stub);
583583
return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS)
584584
.should.eventually.be.rejected.and.have.property(
585-
'message', 'Cannot read property \'done\' of null');
585+
'message', 'Cannot read properties of null (reading \'done\')');
586586
});
587587

588588
it('should reject when API response does not contain a name', () => {
@@ -699,7 +699,7 @@ describe('MachineLearning', () => {
699699
stubs.push(stub);
700700
return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS)
701701
.should.eventually.be.rejected.and.have.property(
702-
'message', 'Cannot read property \'done\' of null');
702+
'message', 'Cannot read properties of null (reading \'done\')');
703703
});
704704

705705
it('should reject when API response does not contain a name', () => {
@@ -803,7 +803,7 @@ describe('MachineLearning', () => {
803803
stubs.push(stub);
804804
return machineLearning.publishModel(MODEL_ID)
805805
.should.eventually.be.rejected.and.have.property(
806-
'message', 'Cannot read property \'done\' of null');
806+
'message', 'Cannot read properties of null (reading \'done\')');
807807
});
808808

809809
it('should reject when API response does not contain a name', () => {
@@ -907,7 +907,7 @@ describe('MachineLearning', () => {
907907
stubs.push(stub);
908908
return machineLearning.unpublishModel(MODEL_ID)
909909
.should.eventually.be.rejected.and.have.property(
910-
'message', 'Cannot read property \'done\' of null');
910+
'message', 'Cannot read properties of null (reading \'done\')');
911911
});
912912

913913
it('should reject when API response does not contain a name', () => {

test/unit/utils/api-request.spec.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -786,9 +786,35 @@ describe('HttpClient', () => {
786786
});
787787
});
788788

789-
it('should not retry when RetryConfig is explicitly null', () => {
789+
it('should succeed, with custom retry config after 1 retry, on a single internal server error response', () => {
790+
mockedRequests.push(mockRequestWithHttpError(500));
791+
const customRetryConfig = {
792+
maxRetries: 4,
793+
statusCodes: [500],
794+
ioErrorCodes: [],
795+
backOffFactor: 0.5,
796+
maxDelayInMillis: 60 * 1000,
797+
};
798+
const respData = { foo: 'bar' };
799+
const scope = nock('https://' + mockHost)
800+
.get(mockPath)
801+
.reply(200, respData, {
802+
'content-type': 'application/json',
803+
});
804+
mockedRequests.push(scope);
805+
const client = new HttpClient(customRetryConfig);
806+
return client.send({
807+
method: 'GET',
808+
url: mockUrl,
809+
}).then((resp) => {
810+
expect(resp.status).to.equal(200);
811+
expect(resp.data).to.deep.equal(respData);
812+
});
813+
});
814+
815+
it('should not retry when retry is explicitly disabled', () => {
790816
mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' }));
791-
const client = new HttpClient(null);
817+
const client = new HttpClient(null, true);
792818
const err = 'Error while making request: connection reset 1';
793819
return client.send({
794820
method: 'GET',

0 commit comments

Comments
 (0)