#Custom Fetch Plugin

1 messages · Page 1 of 1 (latest)

pallid flicker
#

Hi, i created a custom fetch plugin for my external api. I read the document about that: https://nuxt.com/docs/guide/recipes/custom-usefetch, but i have a trouble with JWT validation based on cookies (actually all process about cookies) . As you know we can not send credentials (cookies) with $fetch (https://github.com/nuxt/nuxt/issues/24813) in SSR, but we can work arround this with useRequestFetch. So i tried to create a custom fetch plugin with useRequestFetch, but the problem is when i tried to use useRequestFetch i get Nuxt context error (https://nuxt.com/docs/guide/going-further/nuxt-app#the-nuxt-context). So i create a custom fetch plugin compatible with SSR and client:

GitHub

Describe the feature When having API routes in Nuxt with authentication (using H3 useSession or nuxt-auth-utils), we need to forward the cookie to the API routes when fetching the data on SSR. Not ...

Nuxt

In Nuxt 3, you can access runtime app context within composables, components and plugins.

#

/plugins/api-client.ts

import type { FetchOptions, FetchContext } from 'ofetch'
import { FetchError } from 'ofetch'
import { parseCookies, appendResponseHeader, getResponseHeader } from 'h3'
import defu from 'defu'

export default defineNuxtPlugin({
  name: 'api-client',
  enforce: 'pre',
  async setup(nuxtApp) {
    const config = useRuntimeConfig()

    const getEvent = () => nuxtApp.ssrContext?.event

    const serializeCookies = (cookieString: string) =>
      cookieString
        .split('; ')
        .reduce((cookies: Record<string, string>, cookie) => {
          const [name, ...rest] = cookie.split('=')
          cookies[name] = rest.join('=')
          return cookies
        }, {})

#
const includeCredentials = (options: FetchOptions, filter: string[]) => {
      const event = getEvent()
      if (!event) return

      let cookies = parseCookies(event)
      const setCookieHeader = getResponseHeader(event, 'set-cookie')

      if (setCookieHeader) {
        cookies = defu(serializeCookies(setCookieHeader as string), cookies)
      }

      const filteredCookies = Object.entries(cookies).filter(([name]) =>
        filter.includes(name),
      )
      const cookieString = filteredCookies
        .map(([name, value]) => `${name}=${value}`)
        .join('; ')

      if (cookieString) {
        options.headers = defu({ Cookie: cookieString }, options.headers)
      }
    }

    const handleRequest = async ({ options }: FetchContext) => {
      const event = getEvent()
      if (event) {
        options.credentials = undefined
        includeCredentials(options, ['accessToken', 'refreshToken'])
      } else {
        options.credentials = 'include'
      }
    }

    const handleResponse = async ({ response }: FetchContext) => {
      const event = getEvent()
      if (event && response?.headers.getSetCookie()) {
        response.headers.getSetCookie().forEach((cookie) => {
          appendResponseHeader(event, 'set-cookie', cookie)
        })
      }
    }

    const handleResponseError = async ({ options, response }: FetchContext) => {
      if (response?.status === 401) {
        try {
          await $fetch('/auth/refreshToken', {
            baseURL: options.baseURL,
            method: 'GET',
            credentials: 'include',
            onRequest: handleRequest,
            onResponse: handleResponse,
          })
        } catch (error) {
          if (error instanceof FetchError && error.status === 401) {
            await nuxtApp.runWithContext(() =>
              navigateTo('/logout', { external: true }),
            )
          }
        }
      }
    }
#
const useApi = (options?: FetchOptions) => {
      const defaultOptions: FetchOptions = {
        baseURL: config.public.apiBase,
        headers: {
          'Content-Type': 'application/json',
        },
        retry: 1,
        retryStatusCodes: [401],
        onRequest: handleRequest,
        onResponse: handleResponse,
        onResponseError: handleResponseError,
      }
      return $fetch.create(defu(options, defaultOptions))
    }

    nuxtApp.provide('useApi', useApi)
  },
})
#

Sorry send the code piece by piece, character limit

#

The custom fetch plugin I created works as expected, but I am unsure how production-ready this very convoluted approach is. Is there another pattern or best practice you can recommend instead of this custom fetch plugin (I haven't encountered any)? If not, does anyone have insights into potential issues that might arise with this custom fetch plugin?