Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Clarify that there can be only one retryExchange in docs when calling createClient. #3348

Open
3 tasks done
rfrankspk opened this issue Jul 31, 2023 · 0 comments
Labels
documentation 📖 This needs to be documented but won't need code changes

Comments

@rfrankspk
Copy link

rfrankspk commented Jul 31, 2023

Describe the bug

When calling createClient with an array of exchanges, if there are multiple retryExchanges for errors, then an infinite loop is caused when an error is encountered.

Screen.Recording.2023-07-27.at.2.27.23.PM.mov

Suggestion: The documents provide many great examples of retryExchange usage but it didn't seem to explain the caveat clearly that only one is allowed. Perhaps make this clearer in the documentation, or perhaps it is a bug or unintended consequence.

Here is the doc page we followed...
https://formidable.com/open-source/urql/docs/advanced/retry-operations/

Would love to help if I can! We love the library!

What we tried

/**
 * This exchange is used to retry a GraphQL request if it fails due to a canceled context error.
 * @param opts the options for the retry exchange
 * @returns   An exchange that will retry a GraphQL request if it fails due to a canceled context error
 */
export const canceledContextErrorRetryExchange = ({
  initialDelayMs = 1000,
  maxDelayMs = 15000,
  randomDelay = true,
  maxNumberAttempts = 2,
}): Exchange => {
  return retryExchange({
    initialDelayMs,
    maxDelayMs,
    randomDelay,
    maxNumberAttempts,
    retryIf: (err: any) => err && err.message?.includes("context canceled"),
  });
};

/**
 * This exchange is used to retry a GraphQL request if it fails due to a GraphQL API error.
 * @param opts the options for the retry exchange
 * @returns   An exchange that will retry a GraphQL request if it fails due to a GraphQL API error
 */
export const graphQLErrorRetryExchange = ({
  initialDelayMs = 1000,
  maxDelayMs = 15000,
  randomDelay = true,
  maxNumberAttempts = 2,
}): Exchange => {
  return retryExchange({
    initialDelayMs,
    maxDelayMs,
    randomDelay,
    maxNumberAttempts,
    retryIf: (err: any) => err && err.graphQLErrors?.length > 0,
  });
};

/**
 * This exchange is used to retry a GraphQL request if it fails due to a network error.
 * @param opts the options for the retry exchange
 * @returns   An exchange that will retry a GraphQL request if it fails due to a network error
 */
export const networkErrorRetryExchange = ({
  initialDelayMs = 1000,
  maxDelayMs = 15000,
  randomDelay = true,
  maxNumberAttempts = 2,
}): Exchange => {
  return retryExchange({
    initialDelayMs,
    maxDelayMs,
    randomDelay,
    maxNumberAttempts,
    retryIf: (err: any) => err && err.networkError,
  });
};

export const generateGraphQLClient = (
  configs: DataProviderConfig[],
  environment?: string,
  appId?: string,
  onError?: (error: CombinedError, operation: Operation) => void
): Client => {
  const defaultConfig = configs[0];
  const { url: defaultUrl } = defaultConfig;

  return createClient({
    url: defaultUrl || "http://localhost:4002/graphql",
    fetch: async (input, init) => {
      const controller = new AbortController();
      
      // timeout long running requests after 30 seconds
      const timeout = setTimeout(() => {
        controller.abort();

        onError?.(
          new CombinedError({
            networkError: new Error("Client timed out request to GraphQL API"),
            graphQLErrors: [],
          }),
          null
        );
      }, 30000);

      const response = await fetch(defaultUrl, {
        ...init,
        signal: controller.signal,
      });

      clearInterval(timeout);

      return handleAuthRedirect(response);
    },
    fetchOptions: () => {
      appId = sessionStorage.getItem("application-id") || appId;

      const headers = Object.assign(
        {},
        appId ? { "x-application-id": appId } : {}
      );

      return { headers };
    },
    exchanges: [
      // cacheExchange,
      oktaAuthExchange({ environment }),
      errorExchange({
        onError(error: CombinedError, operation: Operation) {
          // call the onError callback if it exists, which is responsible for bubbling up the error to the nearest error boundary
          onError?.(error, operation);
        },
      }),
      debugExchange({ environment }),
      multipleDataProviderExchange(configs),
      canceledContextErrorRetryExchange({}),
      graphQLErrorRetryExchange({}),
      networkErrorRetryExchange({}),
      fetchExchange,
    ],
  });
};

What ended up working, but we would like finer grained control by providing multiple error retryExchanges.

/**
 * This exchange is used to retry a GraphQL request if it fails due to a network error, context canceled error, or GraphQL API error.
 * @param opts the options for the retry exchange
 * @returns   An exchange that will retry a GraphQL request if it fails due to a specific error
 */
 export const errorRetryExchange = ({
  initialDelayMs = 1000,
  maxDelayMs = 15000,
  randomDelay = true,
  maxNumberAttempts = 2,
}): Exchange => {
  const errorConditions = [
    (err: any)  => err && err.message?.includes("context canceled"), // canceledContextError,
    (err: any) => err && err.graphQLErrors?.length > 0, // graphQLErrorRetryExchange,
    (err: any) => err && err.networkError, // networkErrorRetryExchange,
  ]
  return retryExchange({
    initialDelayMs,
    maxDelayMs,
    randomDelay,
    maxNumberAttempts,
    retryIf: (err: any) => errorConditions.some((condition) => condition(err)),
  });
};

Reproduction

https://github.com/urql-graphql/urql/tree/main/examples/with-retry

Urql version

   "@urql/exchange-auth": "^2.1.5",
    "@urql/exchange-retry": "^1.2.0",
    "urql": "^4.0.4",
    "wonka": "^6.2.3"

Validations

  • I can confirm that this is a bug report, and not a feature request, RFC, question, or discussion, for which GitHub Discussions should be used
  • Read the docs.
  • Follow our Code of Conduct
@kitten kitten added the documentation 📖 This needs to be documented but won't need code changes label Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation 📖 This needs to be documented but won't need code changes
Projects
None yet
Development

No branches or pull requests

2 participants