Skip to content

Commit

Permalink
fix(auth): avoid infinite loop when didAuthError keeps returning true (
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock authored Mar 31, 2023
1 parent f0168ab commit b765903
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-planes-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-auth': patch
---

Avoid infinite loop when `didAuthError` keeps returning true
59 changes: 58 additions & 1 deletion exchanges/auth/src/authExchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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<any>();

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'
);
});
5 changes: 4 additions & 1 deletion exchanges/auth/src/authExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand Down

0 comments on commit b765903

Please sign in to comment.