sindresorhus / ky

🌳 Tiny & elegant JavaScript HTTP client based on the browser Fetch API
MIT License
11.83k stars 341 forks source link

Refresh token with ky: 2 calls instead of 1 #587

Open ericledonge-betonprovincial opened 1 month ago

ericledonge-betonprovincial commented 1 month ago

I don't understand why, when I have a 401 on a http request, my app is calling 2 times the refresh token http endpoint.

Screenshot 2024-05-22 at 3 53 28 PM

My http.services.ts:

import ky, { HTTPError } from "ky";

import { getAccessToken, refreshAccessToken } from "./auth.services";
import { Config } from "../../config";

const createHttpInstance = () => {
  const API_URL = Config.apiUrl;

  const customKy = ky.extend({
    prefixUrl: API_URL,
    hooks: {
      beforeRequest: [
        async (request) => {
          const accessToken = await getAccessToken();
          if (accessToken) {
            request.headers.set("Authorization", `Bearer ${accessToken}`);
            request.headers.set("Content-Type", "application/json");
          }
        },
      ],
      beforeRetry: [
        async ({ request, error, retryCount }) => {
          if (
            error instanceof HTTPError &&
            error.response.status === 401 &&
            retryCount === 1
          ) {
            try {
              const newAccessToken = await refreshAccessToken();
              request.headers.set("Authorization", `Bearer ${newAccessToken}`);
            } catch (error) {
              throw new Error("Failed to refresh token");
            }
          }
        },
      ],
    },
    retry: {
      methods: ["get", "post"],
      limit: 5,
      statusCodes: [401],
    },
  });

  return customKy;
};

export const httpClient = createHttpInstance();

My auth.services.ts:

export const refreshAccessToken = async () => {
  try {
    const accessToken = await getAccessToken();
    const refreshToken = await getRefreshToken();

    if (!accessToken || !refreshToken) {
      await forcedLogout();
      return;
    }

    const response = await httpClient.post(URL_REFRESH_TOKEN, {
      json: { AccessToken: accessToken, RefreshToken: refreshToken },
    });
    const responseData = await response.json();
    const validData = signInResponseSuccessSchema.parse(responseData);

    await saveAccessToken(validData.resource.token);
    await saveRefreshToken(validData.resource.refreshToken);

    return validData.resource.token;
  } catch (error: any) {
    if (error.name === "HTTPError") {
      const errorJson = await error.response.json();
      const errorMessage = getErrorMessage(errorJson);
      logError(errorMessage);
    }

    await forcedLogout();

    throw new Error("Failed to refresh token");
  }
};
sholladay commented 1 week ago

I believe I’ve seen this behavior myself. There’s probably an off-by-one bug somewhere in Ky’s retry logic.

In your beforeRetry hook, does retryCount increment correctly? If you log it, what do you get?

There’s also https://github.com/sindresorhus/ky/issues/233, which is probably unrelated but worth investigating.