Giter VIP home page Giter VIP logo

Comments (22)

AscaL avatar AscaL commented on August 10, 2024 11

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.

pi0 avatar pi0 commented on August 10, 2024 8

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.

attevaltojarvi avatar attevaltojarvi commented on August 10, 2024 7

@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.

Shyam-Chen avatar Shyam-Chen commented on August 10, 2024 7

@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>;
  }
};

image

from ofetch.

attevaltojarvi avatar attevaltojarvi commented on August 10, 2024 6

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.

frasza avatar frasza commented on August 10, 2024 5

@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.

vulpeep avatar vulpeep commented on August 10, 2024 3

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.

 avatar commented on August 10, 2024 3

+1

from ofetch.

RaminderRandhawa91 avatar RaminderRandhawa91 commented on August 10, 2024 3

@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 avatar mrc-bsllt commented on August 10, 2024 2

@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>;
  }
};

image

Hi @Shyam-Chen, how can I use this solution with the useAsyncData?

from ofetch.

Shyam-Chen avatar Shyam-Chen commented on August 10, 2024 1

@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.

Shyam-Chen avatar Shyam-Chen commented on August 10, 2024 1
/**
 * 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.

Shyam-Chen avatar Shyam-Chen commented on August 10, 2024 1

@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.

Denoder avatar Denoder commented on August 10, 2024

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.

reslear avatar reslear commented on August 10, 2024

need https://www.npmjs.com/package/fetch-retry

from ofetch.

mrc-bsllt avatar mrc-bsllt commented on August 10, 2024

Hello guys, is there any news about this feature?
I am having the same problem with Nuxt 3-ohmyfetch-refresh token.
Thanks!

from ofetch.

attevaltojarvi avatar attevaltojarvi commented on August 10, 2024

@mrc-bsllt I've been happy with my custom wrapper approach, give that a try?

from ofetch.

frasza avatar frasza commented on August 10, 2024

Is there any way to make a wrapper/composable so the API using composition (like using useFetch) remains same?

from ofetch.

chrissyast avatar chrissyast commented on August 10, 2024

@frasza

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.

kompetenzlandkarte avatar kompetenzlandkarte commented on August 10, 2024

@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.

pi0 avatar pi0 commented on August 10, 2024

I think such composable would fit in best in nuxt auth module instead of adding to ofetch core size .

from ofetch.

tsotnesharvadze avatar tsotnesharvadze commented on August 10, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.