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): Catch unexpected errors during initialization (#3343)

authored by

Phil Pluckthun and committed by
GitHub
51f67ade 743e5b4b

+111 -58
+5
.changeset/polite-kangaroos-rhyme.md
··· 1 + --- 2 + '@urql/exchange-auth': patch 3 + --- 4 + 5 + `authExchange()` will now block and pass on errors if the initialization function passed to it fails, and will retry indefinitely. It’ll also output a warning for these cases, as the initialization function (i.e. `authExchange(async (utils) => { /*...*/ })`) is not expected to reject/throw.
+30
exchanges/auth/src/authExchange.test.ts
··· 476 476 477 477 expect(res.error).toMatchInlineSnapshot('[CombinedError: [Network] test]'); 478 478 }); 479 + 480 + it('passes on errors during initialization', async () => { 481 + const { source, next } = makeSubject<any>(); 482 + const { exchangeArgs, result } = makeExchangeArgs(); 483 + const init = vi.fn().mockRejectedValue(new Error('oops!')); 484 + const output = vi.fn(); 485 + 486 + pipe(source, authExchange(init)(exchangeArgs), tap(output), publish); 487 + 488 + expect(result).toHaveBeenCalledTimes(0); 489 + expect(output).toHaveBeenCalledTimes(0); 490 + 491 + next(queryOperation); 492 + await new Promise(resolve => setTimeout(resolve)); 493 + expect(result).toHaveBeenCalledTimes(0); 494 + expect(output).toHaveBeenCalledTimes(1); 495 + expect(init).toHaveBeenCalledTimes(1); 496 + expect(output.mock.calls[0][0].error).toMatchInlineSnapshot( 497 + '[CombinedError: [Network] oops!]' 498 + ); 499 + 500 + next(queryOperation); 501 + await new Promise(resolve => setTimeout(resolve)); 502 + expect(result).toHaveBeenCalledTimes(0); 503 + expect(output).toHaveBeenCalledTimes(2); 504 + expect(init).toHaveBeenCalledTimes(2); 505 + expect(output.mock.calls[1][0].error).toMatchInlineSnapshot( 506 + '[CombinedError: [Network] oops!]' 507 + ); 508 + });
+76 -58
exchanges/auth/src/authExchange.ts
··· 227 227 let config: AuthConfig | null = null; 228 228 229 229 return operations$ => { 230 - authPromise = Promise.resolve() 231 - .then(() => 232 - init({ 233 - mutate<Data = any, Variables extends AnyVariables = AnyVariables>( 234 - query: DocumentInput<Data, Variables>, 235 - variables: Variables, 236 - context?: Partial<OperationContext> 237 - ): Promise<OperationResult<Data>> { 238 - const baseOperation = client.createRequestOperation( 239 - 'mutation', 240 - createRequest(query, variables), 241 - context 242 - ); 243 - return pipe( 244 - result$, 245 - onStart(() => { 246 - const operation = addAuthToOperation(baseOperation); 247 - bypassQueue.add( 248 - operation.context._instance as OperationInstance 249 - ); 250 - retries.next(operation); 251 - }), 252 - filter( 253 - result => 254 - result.operation.key === baseOperation.key && 255 - baseOperation.context._instance === 256 - result.operation.context._instance 257 - ), 258 - take(1), 259 - toPromise 260 - ); 261 - }, 262 - appendHeaders( 263 - operation: Operation, 264 - headers: Record<string, string> 265 - ) { 266 - const fetchOptions = 267 - typeof operation.context.fetchOptions === 'function' 268 - ? operation.context.fetchOptions() 269 - : operation.context.fetchOptions || {}; 270 - return makeOperation(operation.kind, operation, { 271 - ...operation.context, 272 - fetchOptions: { 273 - ...fetchOptions, 274 - headers: { 275 - ...fetchOptions.headers, 276 - ...headers, 230 + function initAuth() { 231 + authPromise = Promise.resolve() 232 + .then(() => 233 + init({ 234 + mutate<Data = any, Variables extends AnyVariables = AnyVariables>( 235 + query: DocumentInput<Data, Variables>, 236 + variables: Variables, 237 + context?: Partial<OperationContext> 238 + ): Promise<OperationResult<Data>> { 239 + const baseOperation = client.createRequestOperation( 240 + 'mutation', 241 + createRequest(query, variables), 242 + context 243 + ); 244 + return pipe( 245 + result$, 246 + onStart(() => { 247 + const operation = addAuthToOperation(baseOperation); 248 + bypassQueue.add( 249 + operation.context._instance as OperationInstance 250 + ); 251 + retries.next(operation); 252 + }), 253 + filter( 254 + result => 255 + result.operation.key === baseOperation.key && 256 + baseOperation.context._instance === 257 + result.operation.context._instance 258 + ), 259 + take(1), 260 + toPromise 261 + ); 262 + }, 263 + appendHeaders( 264 + operation: Operation, 265 + headers: Record<string, string> 266 + ) { 267 + const fetchOptions = 268 + typeof operation.context.fetchOptions === 'function' 269 + ? operation.context.fetchOptions() 270 + : operation.context.fetchOptions || {}; 271 + return makeOperation(operation.kind, operation, { 272 + ...operation.context, 273 + fetchOptions: { 274 + ...fetchOptions, 275 + headers: { 276 + ...fetchOptions.headers, 277 + ...headers, 278 + }, 277 279 }, 278 - }, 279 - }); 280 - }, 280 + }); 281 + }, 282 + }) 283 + ) 284 + .then((_config: AuthConfig) => { 285 + if (_config) config = _config; 286 + flushQueue(); 281 287 }) 282 - ) 283 - .then((_config: AuthConfig) => { 284 - if (_config) config = _config; 285 - flushQueue(); 286 - }); 288 + .catch((error: Error) => { 289 + if (process.env.NODE_ENV !== 'production') { 290 + console.warn( 291 + 'authExchange()’s initialization function has failed, which is unexpected.\n' + 292 + 'If your initialization function is expected to throw/reject, catch this error and handle it explicitly.\n' + 293 + 'Unless this error is handled it’ll be passed onto any `OperationResult` instantly and authExchange() will block further operations and retry.', 294 + error 295 + ); 296 + } 297 + 298 + errorQueue(error); 299 + }); 300 + } 301 + 302 + initAuth(); 287 303 288 304 function refreshAuth(operation: Operation) { 289 305 // add to retry queue to try again later ··· 332 348 return operation; 333 349 } else if (operation.context.authAttempt) { 334 350 return addAuthToOperation(operation); 335 - } else if (authPromise) { 336 - if (!retryQueue.has(operation.key)) { 351 + } else if (authPromise || !config) { 352 + if (!authPromise) initAuth(); 353 + 354 + if (!retryQueue.has(operation.key)) 337 355 retryQueue.set( 338 356 operation.key, 339 357 addAuthAttemptToOperation(operation, false) 340 358 ); 341 - } 359 + 342 360 return null; 343 361 } else if (willAuthError(operation)) { 344 362 refreshAuth(operation);