Skip to content

Instantly share code, notes, and snippets.

@kargozeyan
Created April 5, 2026 10:03
Show Gist options
  • Select an option

  • Save kargozeyan/1153a9cb586371ef87ff877315097704 to your computer and use it in GitHub Desktop.

Select an option

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)
// 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