When using interceptors, how to wait some action before continue to another requests?
Describe the feature
Im using ofetch interceptor onResponse to know if the response status code is 401 then I call refresh token, but I have a problem when multiple calls happen at the same time, I want to wait the refresh token call finish in the first request and then continue with the others. This is the code I'm using, the same is mentioned in another closed issue https://github.com/unjs/ofetch/issues/79
import type { FetchRequest, FetchOptions, FetchResponse } from 'ofetch';
import { ofetch } from 'ofetch';
const fetcher = ofetch.create({
baseURL: process.env.API_URL + '/api',
async onRequest({ options }) {
const accessToken = localStorage.getItem('accessToken');
const language = localStorage.getItem('language');
if (accessToken) {
options.headers = {
...options.headers,
Authorization: `Bearer ${accessToken}`,
};
}
if (language) {
options.headers = {
...options.headers,
'Accept-Language': language,
};
}
},
async onResponse({ response }) {
if (response.status === 401 && localStorage.getItem('refreshToken')) {
const { accessToken } = await ofetch('/auth/token', {
baseURL: process.env.API_URL + '/api',
method: 'POST',
body: {
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken'),
},
});
localStorage.setItem('accessToken', accessToken);
}
},
});
export default async <T>(request: FetchRequest, options?: FetchOptions) => {
try {
const response = await fetcher.raw(request, options);
return response as FetchResponse<T>;
} catch (error: any) {
if (error.response?.status === 401 && localStorage.getItem('refreshToken')) {
const response = await fetcher.raw(request, options);
return response as FetchResponse<T>;
}
return error.response as FetchResponse<T>;
}
};
Additional information
- [ ] Would you be willing to help implement this feature?
this is how i handle it in nuxt
import type { FetchOptions } from 'ofetch'
export default defineNuxtPlugin(() => {
const authToken = useCookie('_auth_token')
const refreshToken = useCookie('_refresh_token')
const buildContextRetry = (options: FetchOptions, token: string) => {
const headers = new Headers(options.headers)
headers.set('Authorization', `Bearer ${token}`)
return {
baseURL: options.baseURL,
body: options.body,
headers,
method: options.method,
params: options.params,
query: options.query,
responseType: options.responseType,
ignoreResponseError: options.ignoreResponseError,
parseResponse: options.parseResponse,
duplex: options.duplex,
timeout: options.timeout,
}
}
const api = $fetch.create({
retry: 3,
onRequest({ _request, options, _error }) {
refreshCookie('_auth_token')
if (authToken.value) {
const headers = (options.headers ||= {})
if (Array.isArray(headers)) {
headers.push(['Authorization', `Bearer ${authToken.value}`])
}
else if (headers instanceof Headers) {
headers.set('Authorization', `Bearer ${authToken.value}`)
}
else {
headers.Authorization = `Bearer ${authToken.value}`
}
}
},
onResponse(context) {
if (context.response.status === 401) {
refreshCookie('_auth_token')
return new Promise((resolve, reject) => {
$fetch('/api/auth/refresh-token', {
method: 'POST',
headers: {
authorization: `Bearer ${refreshToken.value}`,
},
})
.then(async ({ data }) => {
const accessTokenExpiredAt = new Date(data.accessTokenExpiredAt)
const refreshTokenExpiredAt = new Date(data.refreshTokenExpiredAt)
useCookie('_auth_token', { expires: accessTokenExpiredAt, default: () => data.accessToken })
useCookie('_refresh_token', { expires: refreshTokenExpiredAt, default: () => data.refreshToken })
const retryOptions = buildContextRetry(context.options, data.accessToken)
refreshCookie('_auth_token')
refreshCookie('_refresh_token')
await resolve($fetch(context.request, {
...retryOptions,
onResponse(ctx) {
Object.assign(context, ctx)
},
}))
})
.catch((error) => {
authToken.value = undefined
refreshToken.value = undefined
reject(error)
return navigateTo('/login')
})
})
}
},
})
// Expose to useNuxtApp().$api
return {
provide: {
api,
},
}
})
Nice! I ended up using it like this
let refreshingToken = false
let requestsQueue: { resolve: any, reject: any }[] = []
const processQueue = (error: any, token: string | null = null) => {
requestsQueue.forEach((prom) => {
if (error) {
prom.reject(error)
} else {
prom.resolve(token)
}
})
requestsQueue = []
}
const apiFetcher = ofetch.create({
baseURL: import.meta.env.VITE_API_BASE_URL + '/api',
timeout: 30000,
retry: false,
credentials: 'include',
async onResponse(ctx) {
if (ctx.response.status === 401) {
if (refreshingToken) {
return new Promise((resolve, reject) => {
requestsQueue.push({ resolve, reject })
})
.then((resp) => {
if (resp) {
ofetch(ctx.request, ctx.options)
}
})
.catch((err) => {
return Promise.reject(err)
})
}
refreshingToken = true
const retry = new Promise((resolve, reject) => {
ofetch.raw<string>('/auth/Refresh-Token', {
baseURL: import.meta.env.VITE_API_BASE_URL + '/api',
method: 'POST',
credentials: 'include'
}).then((resp) => {
processQueue(null, resp._data)
resolve(this)
}).catch(err => {
processQueue(err, null)
const authStore = useAuthStore()
authStore.signOut()
reject(this)
}).finally(() => {
refreshingToken = false
// resolve(this)
})
})
await retry
}
}
})
export default apiFetcher
@enzonun Thanks for posting this code, super helpful! Question : Do you need to resend original request for which you received accessToken expired? And had to trigger refreshToken flow? In other words - requestsQueue.push() is called above only for subsequest requests but not for original request
@enzonun Thanks for posting this code, super helpful! Question : Do you need to resend original request for which you received accessToken expired? And had to trigger refreshToken flow? In other words - requestsQueue.push() is called above only for subsequest requests but not for original request
Yes but because I have a wrapper of the apiFetcher, that does the original request when it fails, this way
import apiFetcher from "./apiFetcher"
const apiFetchRaw = async <T = any>(request: FetchRequest, options?: FetchOptions) => {
try {
const response = await apiFetcher.raw(request, options)
return response as FetchResponse<T>
} catch (error: any) {
if (error.response?.status === 401) {
const response = await apiFetcher.raw(request, options)
return response as FetchResponse<T>
}
return error.response as FetchResponse<T>
}
}
export default apiFetchRaw
I did it in that way because I wanted a version of ofetch.raw, and another wrapper with ofetch<T>, but maybe it can be done in another way
My app is running currently in SSR:false so IDK if it will have some problems on SSR, and I had to add the ofetch explicitly to use the typescript types, so that's another thing that I have to figure it out, maybe I can use nuxt $fetch directly,
I'm closing this because we already figure it out how to do it
太好了!我最终是这样使用的
let refreshingToken = false let requestsQueue: { resolve: any, reject: any }[] = [] const processQueue = (error: any, token: string | null = null) => { requestsQueue.forEach((prom) => { if (error) { prom.reject(error) } else { prom.resolve(token) } }) requestsQueue = [] } const apiFetcher = ofetch.create({ baseURL: import.meta.env.VITE_API_BASE_URL + '/api', timeout: 30000, retry: false, credentials: 'include', async onResponse(ctx) { if (ctx.response.status === 401) { if (refreshingToken) { return new Promise((resolve, reject) => { requestsQueue.push({ resolve, reject }) }) .then((resp) => { if (resp) { ofetch(ctx.request, ctx.options) } }) .catch((err) => { return Promise.reject(err) }) } refreshingToken = true const retry = new Promise((resolve, reject) => { ofetch.raw<string>('/auth/Refresh-Token', { baseURL: import.meta.env.VITE_API_BASE_URL + '/api', method: 'POST', credentials: 'include' }).then((resp) => { processQueue(null, resp._data) resolve(this) }).catch(err => { processQueue(err, null) const authStore = useAuthStore() authStore.signOut() reject(this) }).finally(() => { refreshingToken = false // resolve(this) }) }) await retry } } }) export default apiFetcher
Can this method be used in combination with useAsyncData during SSR?
I've been trying for some time, but I still can't figure out how to refresh token during SSR. I was wondering if you have any new solutions