Comments (22)
Hi, I'm having a similar flow/issue with Nuxt 3 and was wondering if:
- Support for this feature (interceptors with behaviour similar to axios) is coming.
- There is any new functionality that allows me to handle this at this time.
- Should I develop using an alternative solution (maybe nuxt-alt/http as suggested)
Sorry to bother but I'm starting a project with Nuxt 3 at work. I'm just trying to avoid any issues.
Thanks for all the hard work!
Cheers
from ofetch.
Thanks for the feedback @evfoxin @attevaltojarvi
As a summary what I could understand is missing:
- Allow chaining fetch requests using interceptors
- Expose internal type for FetchContext
Actually it is currently possible to achieve this by modification to the context but it is probably more convenience to allow chaining.
from ofetch.
@pi0 I have a follow-up issue with this one.
While waiting for an update for the points raised, I implemented the API client + authentication/token refresh using a recursive approach. It goes something like this:
export const useAPIClient = () => {
const doRequest = async (method, endpoint, config: FetchOptions) => {
const { authClient, refreshSession, invalidateSession } = useAuthProxyClient()
const client = authClient.create({ baseURL: <our API url> })
const authCookie = useCookie('authTokens')
if (authCookie.value) {
config.headers = { ...config.headers, Authorization: `Bearer ${authCookie.value.accessToken}` }
}
try {
return await client(endpoint, { method, ...config })
} catch (requestError) {
const refreshToken = authCookie.value.refreshToken
if (!requestError.response?.status === 401 || !refreshToken) {
// Legitimate 4xx-5xx error, abort
throw requestError
}
try {
await refreshSession(refreshToken)
// call function recursively after refreshSession has done a request to /api/oauth/refresh API route and updated the cookie
return await doRequest(method, endpoint, config)
} catch (refreshError) {
await invalidateSession()
await navigateTo('/login')
}
}
}
return {
doRequest
}
}
export const useAuthProxyClient = () => {
const authClient = $fetch.create({ retry: 0 })
const authCookie = useCookie('auth')
const refreshSession = async refreshToken =>
authClient('/api/oauth/refresh', { method: 'post', body: { refreshToken, ... } })
.then(response => {
return { <access and refresh token values from response> }
})
.then(tokens => { authCookie.value = tokens })
const invalidateSession = async () =>
authClient('/api/oauth/revoke', { method: 'post', body: { ... } })
.then(() => { // ignore errors })
return {
authClient,
refreshSession,
invalidateSession
}
}
The API routes are in Nuxt's server
folder and work correctly when called from client-side. This whole thing works as it should everywhere I normally call it, but during first page load, if the access tokens are not valid anymore, refreshing them doesn't work. Both refreshSession
and invalidateSession
throw a FetchError: Invalid URL ()
, as if the underlying $fetch
instance can't resolve /api/oauth/<whatever>
as a Nuxt server route.
Using the onRequestError interceptor example from the library's README:
async onRequestError ({ request, error }) {
console.log('[fetch request error]', process.server, process.client, request, error)
}
I get
[fetch request error]
true
false
/api/oauth/revoke
TypeError [ERR_INVALID_URL]: Invalid URL
at new NodeError (node:internal/errors:372:5)
at URL.onParseError (node:internal/url:553:9)
at new URL (node:internal/url:629:5)
at new Request (file:///home/atte/Projects/dashboard/node_modules/node-fetch-native/dist/chunks/abort-controller.mjs:5964:16)
at file:///home/atte/Projects/dashboard/node_modules/node-fetch-native/dist/chunks/abort-controller.mjs:6274:19
at new Promise (<anonymous>)
at fetch (file:///home/atte/Projects/dashboard/node_modules/node-fetch-native/dist/chunks/abort-controller.mjs:6272:9)
at $fetchRaw2 (file:///home/atte/Projects/dashboard/node_modules/ohmyfetch/dist/chunks/fetch.mjs:131:26)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
{
input: '/api/oauth/revoke',
code: 'ERR_INVALID_URL'
}
respectively. Really don't know how to fix this, and as said, this only happens on first page load. If I request access tokens successfully, go to our API admin panel and revoke them manually, and then in the Nuxt app go to a different page (I have a page middleware that tries to use the API client to fetch my /me/ endpoint), the whole process works; refreshSession
gets called successfully.
I understand this is not 100% an ohmyfetch issue, but since you also contribute to Nuxt, I thought that you could help me with this.
from ofetch.
@mrc-bsllt wrap ofetch.raw
// request.ts
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>;
}
};
from ofetch.
An update to the above: this was a Nuxt issue. I had import { $fetch } from 'ohmyfetch'
in my client module, and removing that and relying on Nuxt auto-importing it seems to have fixed the issue. The auto-imported $fetch
, though, doesn't have .create()
, so I had to feed the common parameters manually. Not too horrible, but not optimal either.
from ofetch.
@pi0 I see what you mean but this kind of issue has been seen so many times when I searched for the answer. If nothing else, it would be great to add like an example to the docs -- of composable that can handle token refresh, might help a lot of people using separate backend with such strategy. Would add it myself but still haven't fully figured it out.
from ofetch.
Yes, and even more, looks like that some essential types that are required to implement mutating interceptors are not exposed as a public API, so you cannot just wrap ohmyfetch by providing custom implementation of $Fetch
interface.
So the side issue is: please expose all typings that are currently available at error-[hash].d.ts
with some deterministic filename.
Side issue 2: please add some sort of userdata field in FetchContext
to persist arbitrary request state between different hooks.
from ofetch.
+1
from ofetch.
@attevaltojarvi I have been using axios to intercept response and request using the example that you mentioned but now I am trying to use ofetch and needed the same functionality on onRequest and onResponse.
This is what I am doing
const apiFetch = ofetch.create({
baseURL: '/api',
headers: {
Accept: 'application/json'
},
async onRequest({ options }) {
const token = getAuthToken();
if (token && options.headers) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token.accessToken}`,
};
}
},
async onResponse({ response }) {
}
})
Can you please help me with onResponse in ofetch doing same functionality as in axios
apiClient.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
// .isRetry is a non-axios property we use to differentiate the actual request from the one we're firing in this interceptor
if (error.response?.status === 401 && !originalRequest.isRetry) {
originalRequest.isRetry = true
try {
// fetch new tokens from our API
const refreshToken = authStore.refreshToken
const { data } = axios.post('/our/nuxt/server-middleware/auth/route/that/proxies/to/our/api/', { refreshToken })
// simplified for brevity
setNewAccessTokensToStore(data)
// retry the original request
originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${data.refreshToken}` }
return apiClient(originalRequest)
} catch (refreshError) {
return Promise.reject(refreshError)
}
}
}
)
from ofetch.
@mrc-bsllt wrap
ofetch.raw
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>; } };
Hi @Shyam-Chen, how can I use this solution with the useAsyncData?
from ofetch.
@mrc-bsllt I'm not wrapping to the composition API.
<script lang="ts" setup>
import request from '~/utilities/request';
onMounted(async () => {
const response = await request<UserList>('/user-list', { method: 'POST', body: {} });
users.value = response._data.result;
});
</script>
from ofetch.
/**
* wrapping
*/
import { useFetch } from '~/composables';
const todos = useFetch('/todos').json<Todos>();
const todosId = ref<TodoItem['_id']>('');
const todosById = useFetch(computed(() => '/todos/' + todosId.value)).json<TodosById>();
const getTodos = async () => {
await todos.post({}).execute();
console.log(todos.data.value);
};
const getTodoItem = async (id: TodoItem['_id']) => {
todosId.value = id;
await todosById.get().execute();
console.log(todosById.data.value);
};
/**
* not wrapping
*/
import request from '~/utilities/request';
const getTodos = async () => {
const response = await request<Todos>('/todos', { method: 'POST', body: {} });
console.log(response);
};
const getTodoItem = async (id: TodoItem['_id']) => {
const response = await request<TodosById>(`/todos/${id}`, { method: 'GET' });
console.log(response);
};
from ofetch.
@kompetenzlandkarte I'm sorry, but I haven't packaged it in a composable way at the moment. I use import request from '~/utilities/request';
.
The link below shows how I created request.ts
:
https://github.com/Shyam-Chen/Vue-Starter/blob/main/src/utilities/request.ts
from ofetch.
Hey guys I repurposed the @nuxt/http
module to work for nuxt3 and ohmyfetch while also porting axios interceptor-like functionality to it. If you're still interested in this issue, can you take the time to test it out and provide feedback?
https://www.npmjs.com/package/@nuxtjs-alt/http
https://github.com/Teranode/nuxt-module-alternatives/tree/master/@nuxtjs-alt/http
from ofetch.
need https://www.npmjs.com/package/fetch-retry
from ofetch.
Hello guys, is there any news about this feature?
I am having the same problem with Nuxt 3-ohmyfetch-refresh token.
Thanks!
from ofetch.
@mrc-bsllt I've been happy with my custom wrapper approach, give that a try?
from ofetch.
Is there any way to make a wrapper/composable so the API using composition (like using useFetch) remains same?
from ofetch.
Is there any way to make a wrapper/composable so the API using composition (like using useFetch) remains same?
useFetch
is itself an implementation of ofetch
, so the same onRequest
and onResponse
functions can be defined as in @Shyam-Chen's example.
Instead of
ofetch.create({
// ...
async onResponse({response}) {
// yadda yadda yadda
},
// ...
})
you'd use
useFetch(url, {
// ...
async onResponse({response}) {
// blah blah blah
},
// ...
})
,
from ofetch.
@Shyam-Chen thank you for your example provided. Can you please show how the import { useFetch } from '~/composables';
looks like? I am struggeling with combining the fetcher.raw
with the composable.
from ofetch.
I think such composable would fit in best in nuxt auth module instead of adding to ofetch core size .
from ofetch.
Hi Guys,
I Have created new composable for automatic token refresh.
import type {CookieRef, UseFetchOptions} from 'nuxt/app'
import { defu } from 'defu'
export function useCustomFetch<T> (url: string | (() => string), _options: UseFetchOptions<T> = {}) {
const config = useRuntimeConfig()
const tokenAuthUrl = useApiUrl('tokenAuth')
const tokensRefreshUrl = useApiUrl('tokensRefresh')
const userAuth: CookieRef<Record<string, string>> = useCookie('token')
const defaults: UseFetchOptions<T> = {
baseURL: config.public.API_BASE_URL,
retryStatusCodes: [401],
retry: 1,
onRequest ({options}) {
if (userAuth.value?.access){
options.headers = {
...options.headers,
'Authorization': `JWT ${userAuth.value?.access}`
}
}
},
async onResponseError ({response}) {
if (response.status === 401 && response.url !== tokenAuthUrl && response.url !== tokensRefreshUrl && userAuth.value.refresh) {
const response = await $fetch(tokensRefreshUrl, {
baseURL: config.public.API_BASE_URL,
method: 'POST',
body:{
refresh: userAuth.value?.refresh,
}
}).then(
(response) => {
userAuth.value = response
return response
}
).catch((error) => {
console.log(error, 'ErrorRefreshToken')
return error
})
}
}
}
// for nice deep defaults, please use unjs/defu
const params = defu(_options, defaults)
return useFetch(url, params)
}
You can use it as useFetch!
from ofetch.
Related Issues (20)
- deprecate `params` alias for `query` HOT 1
- the HTTP(S) Agent invalid HOT 7
- Can access to typed data in interceptors HOT 2
- Set cookies HOT 11
- error on Response.blob()
- Multipart image upload not working with fetch but with RapidAPI HOT 4
- Missing types in node export HOT 1
- `body` vs `params` option HOT 1
- Inconsistent headers object returned after a retried request
- TS2305: Module 'ofetch' has no exported member FetchOptions HOT 1
- Errors in Interceptors are silently handled
- How to handle errors body HOT 1
- GraphQL Support HOT 2
- Body in options object in $fetch method from NUXT3 not available in request to server HOT 1
- When using $fetch.raw, clone() cannot be used. HOT 2
- unexpected behavior from `ofetch.raw` and `onResponse` HOT 1
- Add support of TS type annotations on `ofetch.create` HOT 3
- Better Handle Support for Development Certificates
- How can ofetch upload files and obtain real-time progress? HOT 1
- define url params using options
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from ofetch.