From b765903767ba904d52272adfb789b0e330d06c35 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 31 Mar 2023 16:21:35 +0200 Subject: [PATCH] fix(auth): avoid infinite loop when didAuthError keeps returning true (#3112) --- .changeset/rotten-planes-heal.md | 5 +++ exchanges/auth/src/authExchange.test.ts | 59 ++++++++++++++++++++++++- exchanges/auth/src/authExchange.ts | 5 ++- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 .changeset/rotten-planes-heal.md diff --git a/.changeset/rotten-planes-heal.md b/.changeset/rotten-planes-heal.md new file mode 100644 index 0000000000..33067e365a --- /dev/null +++ b/.changeset/rotten-planes-heal.md @@ -0,0 +1,5 @@ +--- +'@urql/exchange-auth': patch +--- + +Avoid infinite loop when `didAuthError` keeps returning true diff --git a/exchanges/auth/src/authExchange.test.ts b/exchanges/auth/src/authExchange.test.ts index 7795e3420b..b9045a87d9 100644 --- a/exchanges/auth/src/authExchange.test.ts +++ b/exchanges/auth/src/authExchange.test.ts @@ -158,7 +158,7 @@ it('supports calls to the mutate() method in refreshAuth()', async () => { }, }); - expect(res.operation.context.authAttempt).toBe(false); + expect(res.operation.context.authAttempt).toBe(true); expect(res.operation.context.fetchOptions).toEqual({ method: 'POST', headers: { @@ -385,3 +385,60 @@ it('calls willAuthError on queued operations', async () => { 'final-token' ); }); + +it('does not infinitely retry authentication when an operation did error', async () => { + const { exchangeArgs, result, operations } = makeExchangeArgs(); + const { source, next } = makeSubject(); + + const didAuthError = vi.fn().mockReturnValue(true); + + pipe( + source, + authExchange(async utils => { + let token = 'initial-token'; + return { + addAuthToOperation(operation) { + return utils.appendHeaders(operation, { + Authorization: token, + }); + }, + didAuthError, + async refreshAuth() { + token = 'final-token'; + }, + }; + })(exchangeArgs), + publish + ); + + await new Promise(resolve => setTimeout(resolve)); + + result.mockImplementation(x => ({ + ...queryResponse, + operation: { + ...queryResponse.operation, + ...x, + }, + data: undefined, + error: new CombinedError({ + graphQLErrors: [{ message: 'Oops' }], + }), + })); + + next(queryOperation); + expect(result).toHaveBeenCalledTimes(1); + expect(didAuthError).toHaveBeenCalledTimes(1); + + await new Promise(resolve => setTimeout(resolve)); + + expect(result).toHaveBeenCalledTimes(2); + expect(operations.length).toBe(2); + expect(operations[0]).toHaveProperty( + 'context.fetchOptions.headers.Authorization', + 'initial-token' + ); + expect(operations[1]).toHaveProperty( + 'context.fetchOptions.headers.Authorization', + 'final-token' + ); +}); diff --git a/exchanges/auth/src/authExchange.ts b/exchanges/auth/src/authExchange.ts index 3fdfa5204f..d5a878deef 100644 --- a/exchanges/auth/src/authExchange.ts +++ b/exchanges/auth/src/authExchange.ts @@ -270,6 +270,7 @@ export function authExchange( operation.key, addAuthAttemptToOperation(operation, true) ); + // check that another operation isn't already doing refresh if (config && !authPromise) { authPromise = config.refreshAuth().finally(flushQueue); @@ -310,7 +311,9 @@ export function authExchange( const opsWithAuth$ = pipe( merge([retries.source, pendingOps$]), map(operation => { - if (bypassQueue.has(operation)) { + if (operation.context.authAttempt) { + return addAuthToOperation(operation); + } else if (bypassQueue.has(operation)) { return operation; } else if (authPromise) { if (!retryQueue.has(operation.key)) {