Skip to content

Commit 3e6e396

Browse files
Add a single retry to HttpRequestHandler if it gets a network error. (#85)
1 parent 46f42d6 commit 3e6e396

File tree

3 files changed

+130
-26
lines changed

3 files changed

+130
-26
lines changed

src/utils/api-request.ts

+39-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type ApiCallbackFunction = (data: Object) => void;
3232
export class HttpRequestHandler {
3333
/**
3434
* Sends HTTP requests and returns a promise that resolves with the result.
35+
* Will retry once if the first attempt encounters an AppErrorCodes.NETWORK_ERROR.
3536
*
3637
* @param {string} host The HTTP host.
3738
* @param {number} port The port number.
@@ -50,6 +51,43 @@ export class HttpRequestHandler {
5051
data?: Object,
5152
headers?: Object,
5253
timeout?: number): Promise<Object> {
54+
// Convenience for calling the real _sendRequest() method with the original params.
55+
const sendOneRequest = () => {
56+
return this._sendRequest(host, port, path, httpMethod, data, headers, timeout);
57+
};
58+
59+
return sendOneRequest()
60+
.catch((response: { statusCode: number, error: string|Object }) => {
61+
// Retry if the request failed due to a network error.
62+
if (response.error instanceof FirebaseAppError) {
63+
if ((<FirebaseAppError> response.error).hasCode(AppErrorCodes.NETWORK_ERROR)) {
64+
return sendOneRequest();
65+
}
66+
}
67+
return Promise.reject(response);
68+
});
69+
}
70+
71+
/**
72+
* Sends HTTP requests and returns a promise that resolves with the result.
73+
*
74+
* @param {string} host The HTTP host.
75+
* @param {number} port The port number.
76+
* @param {string} path The endpoint path.
77+
* @param {HttpMethod} httpMethod The http method.
78+
* @param {Object} [data] The request JSON.
79+
* @param {Object} [headers] The request headers.
80+
* @param {number} [timeout] The request timeout in milliseconds.
81+
* @return {Promise<Object>} A promise that resolves with the response.
82+
*/
83+
private _sendRequest(
84+
host: string,
85+
port: number,
86+
path: string,
87+
httpMethod: HttpMethod,
88+
data?: Object,
89+
headers?: Object,
90+
timeout?: number): Promise<Object> {
5391
let requestData;
5492
if (data) {
5593
try {
@@ -189,13 +227,11 @@ export class SignedApiRequestHandler extends HttpRequestHandler {
189227
data: Object,
190228
headers: Object,
191229
timeout: number): Promise<Object> {
192-
let ancestorSendRequest = super.sendRequest;
193-
194230
return this.app_.INTERNAL.getToken().then((accessTokenObj) => {
195231
let headersCopy: Object = deepCopy(headers);
196232
let authorizationHeaderKey = 'Authorization';
197233
headersCopy[authorizationHeaderKey] = 'Bearer ' + accessTokenObj.accessToken;
198-
return ancestorSendRequest(host, port, path, httpMethod, data, headersCopy, timeout);
234+
return super.sendRequest(host, port, path, httpMethod, data, headersCopy, timeout);
199235
});
200236
}
201237
}

src/utils/error.ts

+59-10
Original file line numberDiff line numberDiff line change
@@ -73,30 +73,67 @@ export class FirebaseError extends Error {
7373
}
7474

7575
/**
76-
* Firebase App error code structure. This extends FirebaseError.
76+
* A FirebaseError with a prefix in front of the error code.
7777
*
78+
* @param {string} codePrefix The prefix to apply to the error code.
7879
* @param {string} code The error code.
7980
* @param {string} message The error message.
8081
* @constructor
8182
*/
82-
export class FirebaseAppError extends FirebaseError {
83-
constructor(code: string, message: string) {
83+
class PrefixedFirebaseError extends FirebaseError {
84+
constructor(private codePrefix: string, code: string, message: string) {
8485
super({
85-
code: 'app/' + code,
86+
code: `${codePrefix}/${code}`,
8687
message,
8788
});
89+
90+
/* tslint:disable:max-line-length */
91+
// Set the prototype explicitly. See the following link for more details:
92+
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
93+
/* tslint:enable:max-line-length */
94+
(this as any).__proto__ = PrefixedFirebaseError.prototype;
95+
}
96+
97+
/**
98+
* Allows the error type to be checked without needing to know implementation details
99+
* of the code prefixing.
100+
*
101+
* @param {string} code The non-prefixed error code to test against.
102+
* @return {boolean} True if the code matches, false otherwise.
103+
*/
104+
public hasCode(code: string): boolean {
105+
return `${this.codePrefix}/${code}` === this.code;
106+
}
107+
}
108+
109+
/**
110+
* Firebase App error code structure. This extends PrefixedFirebaseError.
111+
*
112+
* @param {string} code The error code.
113+
* @param {string} message The error message.
114+
* @constructor
115+
*/
116+
export class FirebaseAppError extends PrefixedFirebaseError {
117+
constructor(code: string, message: string) {
118+
super('app', code, message);
119+
120+
/* tslint:disable:max-line-length */
121+
// Set the prototype explicitly. See the following link for more details:
122+
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
123+
/* tslint:enable:max-line-length */
124+
(this as any).__proto__ = FirebaseAppError.prototype;
88125
}
89126
}
90127

91128
/**
92-
* Firebase Auth error code structure. This extends FirebaseError.
129+
* Firebase Auth error code structure. This extends PrefixedFirebaseError.
93130
*
94131
* @param {ErrorInfo} info The error code info.
95132
* @param {string} [message] The error message. This will override the default
96133
* message if provided.
97134
* @constructor
98135
*/
99-
export class FirebaseAuthError extends FirebaseError {
136+
export class FirebaseAuthError extends PrefixedFirebaseError {
100137
/**
101138
* Creates the developer-facing error corresponding to the backend error code.
102139
*
@@ -129,19 +166,25 @@ export class FirebaseAuthError extends FirebaseError {
129166

130167
constructor(info: ErrorInfo, message?: string) {
131168
// Override default message if custom message provided.
132-
super({code: 'auth/' + info.code, message: message || info.message});
169+
super('auth', info.code, message || info.message);
170+
171+
/* tslint:disable:max-line-length */
172+
// Set the prototype explicitly. See the following link for more details:
173+
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
174+
/* tslint:enable:max-line-length */
175+
(this as any).__proto__ = FirebaseAuthError.prototype;
133176
}
134177
}
135178

136179

137180
/**
138-
* Firebase Messaging error code structure. This extends FirebaseError.
181+
* Firebase Messaging error code structure. This extends PrefixedFirebaseError.
139182
*
140183
* @param {ErrorInfo} info The error code info.
141184
* @param {string} [message] The error message. This will override the default message if provided.
142185
* @constructor
143186
*/
144-
export class FirebaseMessagingError extends FirebaseError {
187+
export class FirebaseMessagingError extends PrefixedFirebaseError {
145188
/**
146189
* Creates the developer-facing error corresponding to the backend error code.
147190
*
@@ -174,7 +217,13 @@ export class FirebaseMessagingError extends FirebaseError {
174217

175218
constructor(info: ErrorInfo, message?: string) {
176219
// Override default message if custom message provided.
177-
super({code: 'messaging/' + info.code, message: message || info.message});
220+
super('messaging', info.code, message || info.message);
221+
222+
/* tslint:disable:max-line-length */
223+
// Set the prototype explicitly. See the following link for more details:
224+
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
225+
/* tslint:enable:max-line-length */
226+
(this as any).__proto__ = FirebaseMessagingError.prototype;
178227
}
179228
}
180229

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

+32-13
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@ function mockRequest(
100100
}
101101

102102
/**
103-
* Returns a mocked out error response for a dummy URL.
103+
* Returns a mocked out HTTP error response for a dummy URL.
104104
*
105105
* @param {number} [statusCode] Optional response status code.
106106
* @param {string} [responseContentType] Optional response content type.
107107
* @param {any} [response] Optional response.
108108
*
109109
* @return {Object} A nock response object.
110110
*/
111-
function mockRequestWithError(
111+
function mockRequestWithHttpError(
112112
statusCode = 400,
113113
responseContentType = 'application/json',
114114
response: any = mockErrorResponse,
@@ -124,6 +124,19 @@ function mockRequestWithError(
124124
});
125125
}
126126

127+
/**
128+
* Returns a mocked out error response for a dummy URL, useful for simulating
129+
* network errors.
130+
*
131+
* @param {Error} [err] The request error.
132+
*
133+
* @return {Object} A nock response object.
134+
*/
135+
function mockRequestWithError(err: Error) {
136+
return nock('https://' + mockHost)
137+
.get(mockPath)
138+
.replyWithError(err);
139+
}
127140

128141
describe('HttpRequestHandler', () => {
129142
let mockedRequests: nock.Scope[] = [];
@@ -151,14 +164,12 @@ describe('HttpRequestHandler', () => {
151164

152165

153166
describe('sendRequest', () => {
154-
it('should be rejected on an unexpected error', () => {
155-
httpsRequestStub = sinon.stub(https, 'request');
156-
httpsRequestStub.returns(mockRequestStream);
167+
it('should be rejected, after 1 retry, on multiple network errors', () => {
168+
mockedRequests.push(mockRequestWithError(new Error('first error')));
169+
mockedRequests.push(mockRequestWithError(new Error('second error')));
157170

158171
const sendRequestPromise = httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET');
159172

160-
mockRequestStream.emit('error', new Error('some error'));
161-
162173
return sendRequestPromise
163174
.then(() => {
164175
throw new Error('Unexpected success.');
@@ -168,7 +179,15 @@ describe('HttpRequestHandler', () => {
168179
expect(response.error).to.have.property('code', 'app/network-error');
169180
expect(response.statusCode).to.equal(502);
170181
});
171-
});
182+
});
183+
184+
it('should succeed, after 1 retry, on a single network error', () => {
185+
mockedRequests.push(mockRequestWithError(new Error('first error')));
186+
mockedRequests.push(mockRequest());
187+
188+
return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET')
189+
.should.eventually.be.fulfilled.and.deep.equal(mockSuccessResponse);
190+
});
172191

173192
it('should be rejected on a network timeout', () => {
174193
httpsRequestStub = sinon.stub(https, 'request');
@@ -224,7 +243,7 @@ describe('HttpRequestHandler', () => {
224243

225244
describe('with JSON response', () => {
226245
it('should be rejected given a 4xx response', () => {
227-
mockedRequests.push(mockRequestWithError(400));
246+
mockedRequests.push(mockRequestWithHttpError(400));
228247

229248
return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET')
230249
.should.eventually.be.rejected.and.deep.equal({
@@ -234,7 +253,7 @@ describe('HttpRequestHandler', () => {
234253
});
235254

236255
it('should be rejected given a 5xx response', () => {
237-
mockedRequests.push(mockRequestWithError(500));
256+
mockedRequests.push(mockRequestWithHttpError(500));
238257

239258
return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET')
240259
.should.eventually.be.rejected.and.deep.equal({
@@ -244,7 +263,7 @@ describe('HttpRequestHandler', () => {
244263
});
245264

246265
it('should be rejected given an error when parsing the JSON response', () => {
247-
mockedRequests.push(mockRequestWithError(400, undefined, mockTextErrorResponse));
266+
mockedRequests.push(mockRequestWithHttpError(400, undefined, mockTextErrorResponse));
248267

249268
return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET')
250269
.then(() => {
@@ -275,7 +294,7 @@ describe('HttpRequestHandler', () => {
275294

276295
describe('with text response', () => {
277296
it('should be rejected given a 4xx response', () => {
278-
mockedRequests.push(mockRequestWithError(400, 'text/html'));
297+
mockedRequests.push(mockRequestWithHttpError(400, 'text/html'));
279298

280299
return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET')
281300
.should.eventually.be.rejected.and.deep.equal({
@@ -285,7 +304,7 @@ describe('HttpRequestHandler', () => {
285304
});
286305

287306
it('should be rejected given a 5xx response', () => {
288-
mockedRequests.push(mockRequestWithError(500, 'text/html'));
307+
mockedRequests.push(mockRequestWithHttpError(500, 'text/html'));
289308

290309
return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET')
291310
.should.eventually.be.rejected.and.deep.equal({

0 commit comments

Comments
 (0)