Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(auth): avoid infinite loop when didAuthError keeps returning true (#3112)

authored by

Jovi De Croock and committed by
GitHub
b7659037 f0168aba

+67 -2
+5
.changeset/rotten-planes-heal.md
··· 1 + --- 2 + '@urql/exchange-auth': patch 3 + --- 4 + 5 + Avoid infinite loop when `didAuthError` keeps returning true
+58 -1
exchanges/auth/src/authExchange.test.ts
··· 158 158 }, 159 159 }); 160 160 161 - expect(res.operation.context.authAttempt).toBe(false); 161 + expect(res.operation.context.authAttempt).toBe(true); 162 162 expect(res.operation.context.fetchOptions).toEqual({ 163 163 method: 'POST', 164 164 headers: { ··· 385 385 'final-token' 386 386 ); 387 387 }); 388 + 389 + it('does not infinitely retry authentication when an operation did error', async () => { 390 + const { exchangeArgs, result, operations } = makeExchangeArgs(); 391 + const { source, next } = makeSubject<any>(); 392 + 393 + const didAuthError = vi.fn().mockReturnValue(true); 394 + 395 + pipe( 396 + source, 397 + authExchange(async utils => { 398 + let token = 'initial-token'; 399 + return { 400 + addAuthToOperation(operation) { 401 + return utils.appendHeaders(operation, { 402 + Authorization: token, 403 + }); 404 + }, 405 + didAuthError, 406 + async refreshAuth() { 407 + token = 'final-token'; 408 + }, 409 + }; 410 + })(exchangeArgs), 411 + publish 412 + ); 413 + 414 + await new Promise(resolve => setTimeout(resolve)); 415 + 416 + result.mockImplementation(x => ({ 417 + ...queryResponse, 418 + operation: { 419 + ...queryResponse.operation, 420 + ...x, 421 + }, 422 + data: undefined, 423 + error: new CombinedError({ 424 + graphQLErrors: [{ message: 'Oops' }], 425 + }), 426 + })); 427 + 428 + next(queryOperation); 429 + expect(result).toHaveBeenCalledTimes(1); 430 + expect(didAuthError).toHaveBeenCalledTimes(1); 431 + 432 + await new Promise(resolve => setTimeout(resolve)); 433 + 434 + expect(result).toHaveBeenCalledTimes(2); 435 + expect(operations.length).toBe(2); 436 + expect(operations[0]).toHaveProperty( 437 + 'context.fetchOptions.headers.Authorization', 438 + 'initial-token' 439 + ); 440 + expect(operations[1]).toHaveProperty( 441 + 'context.fetchOptions.headers.Authorization', 442 + 'final-token' 443 + ); 444 + });
+4 -1
exchanges/auth/src/authExchange.ts
··· 270 270 operation.key, 271 271 addAuthAttemptToOperation(operation, true) 272 272 ); 273 + 273 274 // check that another operation isn't already doing refresh 274 275 if (config && !authPromise) { 275 276 authPromise = config.refreshAuth().finally(flushQueue); ··· 310 311 const opsWithAuth$ = pipe( 311 312 merge([retries.source, pendingOps$]), 312 313 map(operation => { 313 - if (bypassQueue.has(operation)) { 314 + if (operation.context.authAttempt) { 315 + return addAuthToOperation(operation); 316 + } else if (bypassQueue.has(operation)) { 314 317 return operation; 315 318 } else if (authPromise) { 316 319 if (!retryQueue.has(operation.key)) {