diff --git a/.changeset/light-kangaroos-smash.md b/.changeset/light-kangaroos-smash.md new file mode 100644 index 0000000000..f3434c7d70 --- /dev/null +++ b/.changeset/light-kangaroos-smash.md @@ -0,0 +1,5 @@ +--- +'@urql/exchange-auth': patch +--- + +Handle `refreshAuth` rejections and pass the resulting error on to `OperationResult`s on the authentication queue. diff --git a/exchanges/auth/src/authExchange.test.ts b/exchanges/auth/src/authExchange.test.ts index b9045a87d9..e4c8a818ac 100644 --- a/exchanges/auth/src/authExchange.test.ts +++ b/exchanges/auth/src/authExchange.test.ts @@ -442,3 +442,37 @@ it('does not infinitely retry authentication when an operation did error', async 'final-token' ); }); + +it('passes on failing refreshAuth() errors to results', async () => { + const { exchangeArgs, result } = makeExchangeArgs(); + + const didAuthError = vi.fn().mockReturnValue(true); + const willAuthError = vi.fn().mockReturnValue(true); + + const res = await pipe( + fromValue(queryOperation), + authExchange(async utils => { + const token = 'initial-token'; + return { + addAuthToOperation(operation) { + return utils.appendHeaders(operation, { + Authorization: token, + }); + }, + didAuthError, + willAuthError, + async refreshAuth() { + throw new Error('test'); + }, + }; + })(exchangeArgs), + take(1), + toPromise + ); + + expect(result).toHaveBeenCalledTimes(0); + expect(didAuthError).toHaveBeenCalledTimes(0); + expect(willAuthError).toHaveBeenCalledTimes(1); + + expect(res.error).toMatchInlineSnapshot('[CombinedError: [Network] test]'); +}); diff --git a/exchanges/auth/src/authExchange.ts b/exchanges/auth/src/authExchange.ts index 13e575c466..ba94f485c8 100644 --- a/exchanges/auth/src/authExchange.ts +++ b/exchanges/auth/src/authExchange.ts @@ -13,6 +13,7 @@ import { import { createRequest, makeOperation, + makeErrorResult, Operation, OperationContext, OperationResult, @@ -202,17 +203,26 @@ export function authExchange( return ({ client, forward }) => { const bypassQueue = new Set(); const retries = makeSubject(); + const errors = makeSubject(); let retryQueue = new Map(); - function flushQueue(_config?: AuthConfig | undefined) { - if (_config) config = _config; + function flushQueue() { authPromise = undefined; const queue = retryQueue; retryQueue = new Map(); queue.forEach(retries.next); } + function errorQueue(error: Error) { + authPromise = undefined; + const queue = retryQueue; + retryQueue = new Map(); + queue.forEach(operation => { + errors.next(makeErrorResult(operation, error)); + }); + } + let authPromise: Promise | void; let config: AuthConfig | null = null; @@ -270,7 +280,10 @@ export function authExchange( }, }) ) - .then(flushQueue); + .then((_config: AuthConfig) => { + if (_config) config = _config; + flushQueue(); + }); function refreshAuth(operation: Operation) { // add to retry queue to try again later @@ -281,7 +294,7 @@ export function authExchange( // check that another operation isn't already doing refresh if (config && !authPromise) { - authPromise = config.refreshAuth().finally(flushQueue); + authPromise = config.refreshAuth().then(flushQueue).catch(errorQueue); } } @@ -341,26 +354,29 @@ export function authExchange( const result$ = pipe(opsWithAuth$, forward); - return pipe( - result$, - filter(result => { - if ( - !bypassQueue.has(result.operation.context._instance) && - result.error && - didAuthError(result) && - !result.operation.context.authAttempt - ) { - refreshAuth(result.operation); - return false; - } + return merge([ + errors.source, + pipe( + result$, + filter(result => { + if ( + !bypassQueue.has(result.operation.context._instance) && + result.error && + didAuthError(result) && + !result.operation.context.authAttempt + ) { + refreshAuth(result.operation); + return false; + } - if (bypassQueue.has(result.operation.context._instance)) { - bypassQueue.delete(result.operation.context._instance); - } + if (bypassQueue.has(result.operation.context._instance)) { + bypassQueue.delete(result.operation.context._instance); + } - return true; - }) - ); + return true; + }) + ), + ]); }; }; }