Skip to content

Commit 6021352

Browse files
committed
Add support for API timeouts to auth-next
1 parent 18c5312 commit 6021352

File tree

3 files changed

+168
-4
lines changed

3 files changed

+168
-4
lines changed

packages-exp/auth-exp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"build": "rollup -c",
1818
"build:deps": "lerna run --scope @firebase/'{app,auth-exp}' --include-dependencies build",
1919
"dev": "rollup -c -w",
20-
"test": "yarn type-check && run-p lint test:browser test:node",
20+
"test": "yarn type-check && run-p lint test:browser",
2121
"test:browser": "karma start --single-run",
2222
"test:browser:debug": "karma start",
2323
"test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha src/**/*.test.* --opts ../../config/mocha.node.opts",
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { FirebaseError } from '@firebase/util';
19+
import { expect, use } from 'chai';
20+
import * as chaiAsPromised from 'chai-as-promised';
21+
import { SinonStub, stub, useFakeTimers } from 'sinon';
22+
import { DEFAULT_API_TIMEOUT, Endpoint, HttpMethod, performApiRequest } from '.';
23+
import { mockEndpoint } from '../../test/api/helper';
24+
import { mockAuth } from '../../test/mock_auth';
25+
import * as mockFetch from '../../test/mock_fetch';
26+
import { ServerError } from './errors';
27+
import { AuthErrorCode } from '../core/errors';
28+
29+
use(chaiAsPromised);
30+
31+
describe('performApiRequest', () => {
32+
const request = {
33+
requestKey: 'request-value'
34+
};
35+
36+
const serverResponse = {
37+
responseKey: 'response-value'
38+
};
39+
40+
context('with regular requests', () => {
41+
beforeEach(mockFetch.setUp);
42+
afterEach(mockFetch.tearDown);
43+
44+
it('should set the correct request, method and HTTP Headers', async () => {
45+
const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse);
46+
const response = await performApiRequest<typeof request, typeof serverResponse>(mockAuth, HttpMethod.POST, Endpoint.SIGN_UP, request);
47+
expect(response).to.eql(serverResponse);
48+
expect(mock.calls.length).to.eq(1);
49+
expect(mock.calls[0].method).to.eq(HttpMethod.POST);
50+
expect(mock.calls[0].request).to.eql(request);
51+
expect(mock.calls[0].headers).to.eql({
52+
'Content-Type': 'application/json'
53+
});
54+
});
55+
56+
it('should translate server errors to auth errors', async () => {
57+
const mock = mockEndpoint(
58+
Endpoint.SIGN_UP,
59+
{
60+
error: {
61+
code: 400,
62+
message: ServerError.EMAIL_EXISTS,
63+
errors: [
64+
{
65+
message: ServerError.EMAIL_EXISTS
66+
}
67+
]
68+
}
69+
},
70+
400
71+
);
72+
const promise = performApiRequest<typeof request, typeof serverResponse>(mockAuth, HttpMethod.POST, Endpoint.SIGN_UP, request);
73+
await expect(promise).to.be.rejectedWith(
74+
FirebaseError,
75+
'Firebase: The email address is already in use by another account. (auth/email-already-in-use).'
76+
);
77+
expect(mock.calls[0].request).to.eql(request);
78+
});
79+
80+
it('should handle unknown server errors', async () => {
81+
const mock = mockEndpoint(
82+
Endpoint.SIGN_UP,
83+
{
84+
error: {
85+
code: 400,
86+
message: 'Awesome error',
87+
errors: [
88+
{
89+
message: 'Awesome error'
90+
}
91+
]
92+
}
93+
},
94+
400
95+
);
96+
const promise = performApiRequest<typeof request, typeof serverResponse>(mockAuth, HttpMethod.POST, Endpoint.SIGN_UP, request);
97+
await expect(promise).to.be.rejectedWith(
98+
FirebaseError,
99+
'Firebase: An internal AuthError has occurred. (auth/internal-error).'
100+
);
101+
expect(mock.calls[0].request).to.eql(request);
102+
});
103+
104+
it('should support custom error handling per endpoint', async () => {
105+
const mock = mockEndpoint(
106+
Endpoint.SIGN_UP,
107+
{
108+
error: {
109+
code: 400,
110+
message: ServerError.EMAIL_EXISTS,
111+
errors: [
112+
{
113+
message: ServerError.EMAIL_EXISTS
114+
}
115+
]
116+
}
117+
},
118+
400
119+
);
120+
const promise = performApiRequest<typeof request, typeof serverResponse>(mockAuth, HttpMethod.POST, Endpoint.SIGN_UP, request, { [ServerError.EMAIL_EXISTS]: AuthErrorCode.ARGUMENT_ERROR });
121+
await expect(promise).to.be.rejectedWith(
122+
FirebaseError,
123+
'Firebase: Error (auth/argument-error).'
124+
);
125+
expect(mock.calls[0].request).to.eql(request);
126+
});
127+
});
128+
129+
context('with network issues', () => {
130+
let fetchStub: SinonStub;
131+
132+
beforeEach(() => {
133+
fetchStub = stub(self, 'fetch');
134+
});
135+
136+
afterEach(() => {
137+
fetchStub.restore();
138+
});
139+
140+
it('should handle timeouts', async () => {
141+
const clock = useFakeTimers();
142+
fetchStub.callsFake(() => {
143+
return new Promise<never>(() => null);
144+
});
145+
const promise = performApiRequest<typeof request, never>(mockAuth, HttpMethod.POST, Endpoint.SIGN_UP, request);
146+
clock.tick(DEFAULT_API_TIMEOUT + 1);
147+
await expect(promise).to.be.rejectedWith(FirebaseError, 'Firebase: The operation has timed out. (auth/timeout).');
148+
clock.restore();
149+
});
150+
151+
it('should handle network failure', async () => {
152+
fetchStub.callsFake(() => {
153+
return new Promise<never>((_, reject) => reject(new Error('network error')));
154+
});
155+
const promise = performApiRequest<typeof request, never>(mockAuth, HttpMethod.POST, Endpoint.SIGN_UP, request);
156+
await expect(promise).to.be.rejectedWith(FirebaseError, 'Firebase: A network AuthError (such as timeout]: interrupted connection or unreachable host) has occurred. (auth/network-request-failed).');
157+
});
158+
});
159+
});

packages-exp/auth-exp/src/api/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export enum Endpoint {
5353
WITHDRAW_MFA = '/v2/accounts/mfaEnrollment:withdraw'
5454
}
5555

56+
export const DEFAULT_API_TIMEOUT = 30_000;
57+
5658
export async function performApiRequest<T, V>(
5759
auth: Auth,
5860
method: HttpMethod,
@@ -67,7 +69,7 @@ export async function performApiRequest<T, V>(
6769
body: JSON.stringify(request)
6870
}
6971
: {};
70-
const response = await fetch(
72+
const response: Response = await Promise.race<Promise<Response>>([fetch(
7173
`${auth.config.apiScheme}://${auth.config.apiHost}${path}?key=${auth.config.apiKey}`,
7274
{
7375
method,
@@ -77,7 +79,9 @@ export async function performApiRequest<T, V>(
7779
referrerPolicy: 'no-referrer',
7880
...body
7981
}
80-
);
82+
), new Promise((_, reject) =>
83+
setTimeout(() => reject(AUTH_ERROR_FACTORY.create(AuthErrorCode.TIMEOUT, { appName: auth.name })), DEFAULT_API_TIMEOUT)
84+
)]);
8185
if (response.ok) {
8286
return response.json();
8387
} else {
@@ -88,7 +92,8 @@ export async function performApiRequest<T, V>(
8892
} else {
8993
// TODO probably should handle improperly formatted errors as well
9094
// If you see this, add an entry to SERVER_ERROR_MAP for the corresponding error
91-
throw new Error(`Unexpected API error: ${json.error.message}`);
95+
console.error(`Unexpected API error: ${json.error.message}`);
96+
throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, { appName: auth.name });
9297
}
9398
}
9499
} catch (e) {

0 commit comments

Comments
 (0)