0

I am having a dumb moment. I use Tanstack Query and Ky in my app to make requests and inject jwt tokens in them. Every time a request hits a 403 error, the request gets resent after acquiring a new access token.

The annoying thing is when 403 occurs it shows a 403 error in the console originating from timeout.ts. And the stack goes through the whole request proccess from Ky to Tanstack Query and I don't know here to handle it so I can just post 'Getting new token' to console instead of this error.

I tried checking response.status in const request in useInfiniteLogsQuery, tried ky's beforeError hook and tinkered with the response in customKy but nothing seems to affect this error.

GET http://localhost:8080/api/v1/users/root/ 403 (Forbidden) timeout.ts:24

Here's the QueryClient:

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PropsWithChildren, useState } from "react";

const QueryProvider = ({ children }: PropsWithChildren) => {
  const [queryClientInstance] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 1000 * 30,
            retry: false,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClientInstance}>
      {children}
    </QueryClientProvider>
  );
};

export default QueryProvider;

Here's the custom ky instance I use:

import useAuth from "hooks/useAuth";
import ky from "ky";

export const useKy = () => {
  const { auth, setAuth, refreshToken } = useAuth();

  const customKy = ky.extend({
    prefixUrl: "api/v1",
    hooks: {
      beforeRequest: [
        async (request) => {
          if (auth)
            request.headers.set("Authorization", `Bearer ${auth.access_token}`);
        },
      ],
      afterResponse: [
        async (request, _, response) => {
          if (response.status === 403) {
            console.log(response);

            try {
              await refreshToken().then(({ access_token, username }) => {
                setAuth({ access_token, username });
                request.headers.set("Authorization", `Bearer ${access_token}`);
              });

              return ky(request);
            } catch (error) {
              throw new Error("Failed to refresh token: " + error);
            }
          }
        },
      ],
    },
    retry: {
      methods: ["get", "post"],
      limit: 3,
      statusCodes: [403],
    },
  });

  return { customKy };
};

One of the queries:

export const useInfiniteLogsQuery = () => {
  const { customKy } = useKy();
  return useInfiniteQuery({
    queryKey: ["infiniteLogs"],
    queryFn: async ({ pageParam }) => {
      const link = pageParam.split("api/v1/")[1];
      const resolvedLink = `${link}&resolved=false`;
      const request = await customKy.get<LogsResponse>(resolvedLink).json();

      return request;
    },
    initialPageParam: `api/v1/logs-list/json/?limit=25&offset=0`,
    getNextPageParam: (lastPage) => lastPage?.next,
  });
};

enter image description here

2
  • I've added timeout: false to ky.extend and now the error doesn't show timeout.ts but shows Ky.ts. I guess the error is from Ky but the question still stands. Commented Apr 25 at 8:11
  • Stumbled upon this. Timeout errors don't get into beforeError hook. Commented Apr 25 at 8:12

1 Answer 1

1

Actually here's a better answer:

The afterResponse hook is good for retry logic, but the error is already thrown by the time you see it in TanStack Query.

The beforeRetry hook is called before Ky retries the request, and you can log or modify things here.

If you want to completely suppress the error, you need to ensure that the error is handled in Ky and not thrown up to TanStack Query unless you truly want to fail.

export const useKy = () => {
  const { auth, setAuth, refreshToken } = useAuth();

  const customKy = ky.extend({
    prefixUrl: 'api/v1',
    hooks: {
      beforeRequest: [
        async (request) => {
          if (auth)
            request.headers.set('Authorization', `Bearer ${auth.access_token}`);
        },
      ],
      beforeRetry: [
        async ({ request, options, error, retryCount }) => {
          if (error instanceof ky.HTTPError && error.response.status === 403 && retryCount === 1) {
            console.log('Getting new token');
            try {
              const { access_token, username } = await refreshToken();
              setAuth({ access_token, username });
              request.headers.set('Authorization', `Bearer ${access_token}`);
            } catch (refreshError) {
              throw new Error('Failed to refresh token: ' + refreshError);
            }
          }
        },
      ],
    },
    retry: {
      methods: ['get', 'post'],
      limit: 3,
      statusCodes: [403],
    },
  });

  return { customKy };
};

The afterResponse hook is good for retry logic, but the error is already thrown by the time you see it in TanStack Query.

The beforeRetry hook is called before Ky retries the request, and you can log or modify things here.

If you want to completely suppress the error, you need to ensure that the error is handled in Ky and not thrown up to TanStack Query unless you truly want to fail.

Sign up to request clarification or add additional context in comments.

1 Comment

I am grateful for you helping out but I tried beforeRetry hook before and when the access token expires it doesn't refetch it on retries. For example, I refresh the page and get the token, I can send requests, after a minute access token expires and refetch logic kicks in but I get 403 errors and the token never updates. And the error is still there. I am wondering where this timeout.ts comes from. It's not the code I've written. Probably from tanstack. Maybe it connected to retry setting in QueryClient?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.