Created
April 5, 2026 10:03
-
-
Save kargozeyan/1153a9cb586371ef87ff877315097704 to your computer and use it in GitHub Desktop.
Nuxt plugin for handling access/refresh token cookies with external backend (SSR-compatible)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // app/plugins/api.ts | |
| import { appendResponseHeader } from 'h3'; | |
| import { parseCookie, parseSetCookie, stringifyCookie } from 'cookie'; | |
| export default defineNuxtPlugin(() => { | |
| const event = import.meta.server ? useRequestEvent() : undefined; | |
| // During SSR, we parse the initial browser cookies. During the request lifecycle, | |
| // we update this object with new cookies from backend responses (e.g. after a token refresh). | |
| const cookies = import.meta.server && event ? parseCookie(event.node.req.headers.cookie ?? '') : {}; | |
| // Used to deduplicate concurrent refresh requests so we only hit the endpoint once | |
| let refreshPromise: Promise<void> | null = null; | |
| function forwardCookies(response: Response) { | |
| if (!import.meta.server || !event) return; | |
| const setCookieHeaders = | |
| typeof response.headers.getSetCookie === 'function' | |
| ? response.headers.getSetCookie() | |
| : Array.from<[string, string]>(response.headers.entries()) | |
| .filter(([k]) => k.toLowerCase() === 'set-cookie') | |
| .map(([, v]) => v); | |
| for (const setCookie of setCookieHeaders) { | |
| // 1. Send the cookie back to the browser | |
| appendResponseHeader(event, 'set-cookie', setCookie); | |
| // 2. Update our in-memory cookie map so subsequent requests | |
| // within the same SSR pass use the newly updated token. | |
| const { name, value } = parseSetCookie(setCookie); | |
| cookies[name] = value; | |
| } | |
| } | |
| function isRefreshRequest(request: Request | string): boolean { | |
| const url = request instanceof Request ? request.url : request; | |
| return url.includes('/auth/refresh'); | |
| } | |
| const api = $fetch.create({ | |
| baseURL: '/api', | |
| onRequest({ options }) { | |
| if (import.meta.server) { | |
| // Forward the cookies to the backend. Stringifying the map here | |
| // ensures that new cookies set during this SSR pass are included. | |
| options.headers.set('Cookie', stringifyCookie(cookies)); | |
| } | |
| }, | |
| async onResponse(ctx) { | |
| forwardCookies(ctx.response); | |
| // Only intercept 401s, and don't intercept if the refresh request itself fails | |
| if (ctx.response.status !== 401 || isRefreshRequest(ctx.request)) return; | |
| try { | |
| if (!refreshPromise) { | |
| refreshPromise = api('/auth/refresh', { method: 'POST' }) | |
| .then(() => { }) | |
| .finally(() => { | |
| refreshPromise = null; | |
| }); | |
| } | |
| await refreshPromise; | |
| // Retry the original request. The cookies map was already updated | |
| // by forwardCookies when the refresh request succeeded, so onRequest | |
| // will naturally attach the new token. | |
| ctx.response = await api.raw(ctx.request, ctx.options as any); | |
| } catch { | |
| // If refresh fails, fall back to logout and redirect | |
| await api('/auth/logout', { method: 'POST' }).catch(() => { }); | |
| if (import.meta.client) { | |
| await navigateTo('/login'); | |
| } | |
| } | |
| } | |
| }); | |
| return { provide: { api } }; | |
| }); | |
| // app/composables/api | |
| export const useApi: typeof useFetch = (url, options) => { | |
| return useFetch(url, { | |
| ...options, | |
| $fetch: useNuxtApp().$api | |
| }); | |
| }; | |
| // Proxy configuration | |
| routeRules: { | |
| '/api/**': { | |
| proxy: { | |
| to: `${process.env.API_BASE_URL || 'http://localhost:8080'}/**` | |
| }, | |
| cors: true | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment