N
Nuxt3mo ago
Mediv0

Implement http-only jwt tokens with retry

Hi, I’m trying to refresh the access token when I receive a 401 error and retry all pending requests. I could use onResponse and onRequest. Something like this:
const $api = (endpoint) => {
useFetch(endpoint, {
onResponse: async (context) => {
const { statusCode } = context;
if (statusCode === 401) {
try {
await refreshToken();
retryWith(options);
} catch (e) {
navigateTo("/auth/login");
}
}
},
});
};
const $api = (endpoint) => {
useFetch(endpoint, {
onResponse: async (context) => {
const { statusCode } = context;
if (statusCode === 401) {
try {
await refreshToken();
retryWith(options);
} catch (e) {
navigateTo("/auth/login");
}
}
},
});
};
This works fine when you only have one request at a time, but when you have multiple requests on the page:
<script setup>
$api("/api/1");
$api("/api/2");
</script>
<script setup>
$api("/api/1");
$api("/api/2");
</script>
It just doesn't work (multiple refreshToken calls). Any ideas on how to approach this?
2 Replies
Mediv0
Mediv0OP3mo ago
I was thinking about something like:
const useApi = () => {
const isRequestingNewToken = false;
const queue = [];

const $api = $fetch.create({
baseURL: config.public.api,
credentials: "include",
retry: 1,
retryStatusCodes: [401],

onResponse: async ({ response, options, request }) => {
if (response.status === 401) {
try {
if (isRequestingNewToken)
return;
await refreshToken();

queue.forEach((config) => {
retryWith(config)
})

}
catch (error) {
options.retry = false;
await authStore.logout();
console.error("Token refresh failed:", error);
}
}
},
});

return $api;
};
const useApi = () => {
const isRequestingNewToken = false;
const queue = [];

const $api = $fetch.create({
baseURL: config.public.api,
credentials: "include",
retry: 1,
retryStatusCodes: [401],

onResponse: async ({ response, options, request }) => {
if (response.status === 401) {
try {
if (isRequestingNewToken)
return;
await refreshToken();

queue.forEach((config) => {
retryWith(config)
})

}
catch (error) {
options.retry = false;
await authStore.logout();
console.error("Token refresh failed:", error);
}
}
},
});

return $api;
};
not sure if this will work " these are not actual codes " This is the code we used for one of our internal services ( React, next.js )
const API_URL = process.env.NEXT_PUBLIC_API_URL;
let isRefreshing = false;
const refreshAndRetryQueue: RetryQueueItem[] = [];

const createApiClient = () => {
const axiosConfig: any = {
baseURL: API_URL,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
};

return axios.create(axiosConfig);
};

const client = createApiClient();

client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest: AxiosRequestConfig = error.config;

if (error.response.status === 401 error.response.status === 403) {
if (!isRefreshing) {
isRefreshing = true;
try {
// Refresh the access token
await refreshAccessToken();

refreshAndRetryQueue.forEach(({ config, resolve, reject }) => {
client
.request(config)
.then((response) => resolve(response))
.catch((err) => reject(err));
});

// Clear the queue
refreshAndRetryQueue.length = 0;
return client(originalRequest);

} catch (refreshError) {
throw refreshError;
} finally {
isRefreshing = false;
}
}

// Add the original request to the queue
return new Promise<void>((resolve, reject) => {
refreshAndRetryQueue.push({ config: originalRequest, resolve, reject });
});
}

return Promise.reject(error);
}
);

export { client };
const API_URL = process.env.NEXT_PUBLIC_API_URL;
let isRefreshing = false;
const refreshAndRetryQueue: RetryQueueItem[] = [];

const createApiClient = () => {
const axiosConfig: any = {
baseURL: API_URL,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
};

return axios.create(axiosConfig);
};

const client = createApiClient();

client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest: AxiosRequestConfig = error.config;

if (error.response.status === 401 error.response.status === 403) {
if (!isRefreshing) {
isRefreshing = true;
try {
// Refresh the access token
await refreshAccessToken();

refreshAndRetryQueue.forEach(({ config, resolve, reject }) => {
client
.request(config)
.then((response) => resolve(response))
.catch((err) => reject(err));
});

// Clear the queue
refreshAndRetryQueue.length = 0;
return client(originalRequest);

} catch (refreshError) {
throw refreshError;
} finally {
isRefreshing = false;
}
}

// Add the original request to the queue
return new Promise<void>((resolve, reject) => {
refreshAndRetryQueue.push({ config: originalRequest, resolve, reject });
});
}

return Promise.reject(error);
}
);

export { client };
But implementing a global state like this:
let isRefreshing = false;
const refreshAndRetryQueue: RetryQueueItem[] = [];
let isRefreshing = false;
const refreshAndRetryQueue: RetryQueueItem[] = [];
in an SSR app doesn't make sense, as it causes the state to be shared across all users, if I'm not mistaken?
Cue
Cue3mo ago
useState or Pinia is your friend.
Want results from more Discord servers?
Add your server