Closed enzonun closed 2 days ago
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
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
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
Additional information