Skip to content

Commit a5c44c5

Browse files
committed
Add network timeout cancellation to the core network code (#3771)
* Add network timeout cancellation to core network code * Formatting * Fix broken test on CI * Formatting
1 parent 83759dc commit a5c44c5

File tree

3 files changed

+48
-12
lines changed

3 files changed

+48
-12
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import { assert, expect, use } from 'chai';
1919
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
2021
import { useFakeTimers } from 'sinon';
22+
import * as sinonChai from 'sinon-chai';
2123

2224
import { FirebaseError } from '@firebase/util';
2325

@@ -36,6 +38,7 @@ import {
3638
} from './';
3739
import { ServerError } from './errors';
3840

41+
use(sinonChai);
3942
use(chaiAsPromised);
4043

4144
describe('api/_performApiRequest', () => {
@@ -227,6 +230,22 @@ describe('api/_performApiRequest', () => {
227230
clock.restore();
228231
});
229232

233+
it('should clear the network timeout on success', async () => {
234+
const clock = useFakeTimers();
235+
sinon.spy(clock, 'clearTimeout');
236+
mockFetch.setUp();
237+
mockEndpoint(Endpoint.SIGN_UP, {});
238+
const promise = _performApiRequest(
239+
auth,
240+
HttpMethod.POST,
241+
Endpoint.SIGN_UP,
242+
request
243+
);
244+
await promise;
245+
expect(clock.clearTimeout).to.have.been.called;
246+
clock.restore();
247+
});
248+
230249
it('should handle network failure', async () => {
231250
mockFetch.setUpWithOverride(() => {
232251
return new Promise<never>((_, reject) =>

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,16 @@ export async function _performFetchWithErrorHandling<V>(
120120
(auth as Auth)._canInitEmulator = false;
121121
const errorMap = { ...SERVER_ERROR_MAP, ...customErrorMap };
122122
try {
123+
const networkTimeout = new NetworkTimeout<Response>(auth.name);
123124
const response: Response = await Promise.race<Promise<Response>>([
124125
fetchFn(),
125-
makeNetworkTimeout(auth.name)
126+
networkTimeout.promise
126127
]);
127128

129+
// If we've reached this point, the fetch succeeded and the networkTimeout
130+
// didn't throw; clear the network timeout delay so that Node won't hang
131+
networkTimeout.clearNetworkTimeout();
132+
128133
const json = await response.json();
129134
if ('needConfirmation' in json) {
130135
throw makeTaggedError(auth, AuthErrorCode.NEED_CONFIRMATION, json);
@@ -201,16 +206,26 @@ export function _getFinalTarget(
201206
return _emulatorUrl(auth.config, base);
202207
}
203208

204-
function makeNetworkTimeout<T>(appName: string): Promise<T> {
205-
return new Promise((_, reject) =>
206-
setTimeout(() => {
209+
class NetworkTimeout<T> {
210+
// Node timers and browser timers are fundamentally incompatible, but we
211+
// don't care about the value here
212+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
213+
private timer: any | null = null;
214+
readonly promise = new Promise<T>((_, reject) => {
215+
this.timer = setTimeout(() => {
207216
return reject(
208217
AUTH_ERROR_FACTORY.create(AuthErrorCode.TIMEOUT, {
209-
appName
218+
appName: this.appName
210219
})
211220
);
212-
}, DEFAULT_API_TIMEOUT_MS.get())
213-
);
221+
}, DEFAULT_API_TIMEOUT_MS.get());
222+
});
223+
224+
clearNetworkTimeout(): void {
225+
clearTimeout(this.timer);
226+
}
227+
228+
constructor(private readonly appName: string) {}
214229
}
215230

216231
interface PotentialResponse extends IdTokenResponse {

packages-exp/auth-exp/src/core/strategies/email.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Operation, ProviderId } from '@firebase/auth-types-exp';
2424
import { FirebaseError, isNode } from '@firebase/util';
2525

2626
import { mockEndpoint } from '../../../test/helpers/api/helper';
27-
import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth';
27+
import { testAuth, TestAuth, testUser } from '../../../test/helpers/mock_auth';
2828
import * as mockFetch from '../../../test/helpers/mock_fetch';
2929
import { Endpoint } from '../../api';
3030
import { ServerError } from '../../api/errors';
@@ -72,10 +72,12 @@ describe('core/strategies/fetchSignInMethodsForEmail', () => {
7272
});
7373
const response = await fetchSignInMethodsForEmail(auth, email);
7474
expect(response).to.eql(expectedSignInMethods);
75-
expect(mock.calls[0].request).to.eql({
76-
identifier: email,
77-
continueUri: 'http://localhost:8089/context.html'
78-
});
75+
const request = mock.calls[0].request as Record<string, string>;
76+
expect(request['identifier']).to.eq(email);
77+
// We can't rely on a fixed port number
78+
expect(request['continueUri']).to.match(
79+
/http:\/\/localhost:[0-9]+\/context\.html/
80+
);
7981
});
8082
}
8183

0 commit comments

Comments
 (0)