N
Nuxtβ€’3mo ago
RicharDVD

Pinia state leaks across browser sessions

I have a pretty severe issue where data stored in Pinia is sometimes leaked across requests. I have a store that is persisted in cookies using the @pinia-plugin-persistedstate package. The state of this is sometimes set across all users so all users have the same pinia data. Even though their cookies are empty, when looking at
__NUXT__.pinia
__NUXT__.pinia
the state is set to that of another user. Any idea's how this can accur?
32 Replies
manniL
manniLβ€’3mo ago
do you use pinia in your plugins?
RicharDVD
RicharDVDOPβ€’3mo ago
I do use it in a plugin that wraps the basic $fetch to always send the authentication headers along with it like follows:
import type { LoginResponse } from '~/utils/types';

export default defineNuxtPlugin(() => {
const runtimeConfig = useRuntimeConfig();
const baseURL = runtimeConfig.public.apiBaseUrl || 'localhost:8000/api';

const $api = $fetch.create({
baseURL,
retryStatusCodes: [401],
retry: 1,
onRequest({ options }) {
const authStore = useAuthStore();
if (authStore.isLoggedIn && authStore.accessToken) {
options.headers = {
...options.headers,
Authorization: `Bearer ${authStore.accessToken}`,
};
}
},
onResponseError: ({ response }) => {
const authStore = useAuthStore();
if (response.status === 401) {
return $fetch<LoginResponse>('/auth/refresh', {
method: 'POST',
baseURL,
credentials: 'same-origin',
headers: {
'Authorization': `Bearer ${authStore.refreshToken}`,
'Access-Control-Allow-Origin': '*',
},
body: {
refresh_token: authStore.refreshToken,
},
}).then((response) => {
authStore.signIn(response);
}).catch(async () => {
authStore.logout();
await navigateTo('/login');
});
}
},
});

return {
provide: {
api: $api,
},
};
});
import type { LoginResponse } from '~/utils/types';

export default defineNuxtPlugin(() => {
const runtimeConfig = useRuntimeConfig();
const baseURL = runtimeConfig.public.apiBaseUrl || 'localhost:8000/api';

const $api = $fetch.create({
baseURL,
retryStatusCodes: [401],
retry: 1,
onRequest({ options }) {
const authStore = useAuthStore();
if (authStore.isLoggedIn && authStore.accessToken) {
options.headers = {
...options.headers,
Authorization: `Bearer ${authStore.accessToken}`,
};
}
},
onResponseError: ({ response }) => {
const authStore = useAuthStore();
if (response.status === 401) {
return $fetch<LoginResponse>('/auth/refresh', {
method: 'POST',
baseURL,
credentials: 'same-origin',
headers: {
'Authorization': `Bearer ${authStore.refreshToken}`,
'Access-Control-Allow-Origin': '*',
},
body: {
refresh_token: authStore.refreshToken,
},
}).then((response) => {
authStore.signIn(response);
}).catch(async () => {
authStore.logout();
await navigateTo('/login');
});
}
},
});

return {
provide: {
api: $api,
},
};
});
Considering your point, I assume this is not SSR safe than?πŸ˜… Is there another way I should call useAuthStore()? Would passing the pinia instance to the store as follows fix this issue @manniL / TheAlexLichter
export default defineNuxtPlugin((ctx) => {
const runtimeConfig = useRuntimeConfig();
const baseURL = runtimeConfig.public.apiBaseUrl || 'localhost:8000/api';
const $pinia = ctx.$pinia as Pinia;

const $api = $fetch.create({
baseURL,
retryStatusCodes: [401],
retry: 1,
onRequest({ options }) {
const authStore = useAuthStore($pinia);
if (authStore.isLoggedIn && authStore.accessToken) {
options.headers = {
...options.headers,
Authorization: `Bearer ${authStore.accessToken}`,
};
}
},
onResponseError: ({ response }) => {
const authStore = useAuthStore($pinia);
if (response.status === 401) {
return $fetch<LoginResponse>('/auth/refresh', {
method: 'POST',
baseURL,
credentials: 'same-origin',
headers: {
'Authorization': `Bearer ${authStore.refreshToken}`,
'Access-Control-Allow-Origin': '*',
},
body: {
refresh_token: authStore.refreshToken,
},
}).then((response) => {
authStore.signIn(response);
}).catch(async () => {
authStore.logout();
await navigateTo('/login');
});
}
},
});

return {
provide: {
api: $api,
},
};
});
export default defineNuxtPlugin((ctx) => {
const runtimeConfig = useRuntimeConfig();
const baseURL = runtimeConfig.public.apiBaseUrl || 'localhost:8000/api';
const $pinia = ctx.$pinia as Pinia;

const $api = $fetch.create({
baseURL,
retryStatusCodes: [401],
retry: 1,
onRequest({ options }) {
const authStore = useAuthStore($pinia);
if (authStore.isLoggedIn && authStore.accessToken) {
options.headers = {
...options.headers,
Authorization: `Bearer ${authStore.accessToken}`,
};
}
},
onResponseError: ({ response }) => {
const authStore = useAuthStore($pinia);
if (response.status === 401) {
return $fetch<LoginResponse>('/auth/refresh', {
method: 'POST',
baseURL,
credentials: 'same-origin',
headers: {
'Authorization': `Bearer ${authStore.refreshToken}`,
'Access-Control-Allow-Origin': '*',
},
body: {
refresh_token: authStore.refreshToken,
},
}).then((response) => {
authStore.signIn(response);
}).catch(async () => {
authStore.logout();
await navigateTo('/login');
});
}
},
});

return {
provide: {
api: $api,
},
};
});
manniL
manniLβ€’3mo ago
That’s what I’d suggest
RicharDVD
RicharDVDOPβ€’2mo ago
Thank you so much man! We unfortunately encountered the same issue so this didn't seem to be the fix. Do you maybe have any other idea's that could cause this? It's very hard to reproduce this since it only happens once in a while and I am not sure what circumstances cause this to happen.
Dovendyret
Dovendyretβ€’2mo ago
@RicharDVD Do you define and import your default state from a separate file?
RicharDVD
RicharDVDOPβ€’2mo ago
@Dovendyret The store looks as follows:
import { defineStore } from 'pinia';
import type { UserProfile } from '~/utils/types';

export const useAuthStore = defineStore(
'auth',
() => {
const accessToken = ref('');
const refreshToken = ref('');
const profile = ref<UserProfile | null>(null);

const isLoggedIn = computed(() => !!accessToken.value);

function signIn(login: LoginResponse) {
accessToken.value = login.access_token;
refreshToken.value = login.refresh_token;
profile.value = login.user;
}

function logout() {
accessToken.value = '';
refreshToken.value = '';
profile.value = null;
}

return {
accessToken,
refreshToken,
profile,
logout,
isLoggedIn,
signIn,
};
},
{
persist: {
storage: piniaPluginPersistedstate.cookies(),
},
},
);
import { defineStore } from 'pinia';
import type { UserProfile } from '~/utils/types';

export const useAuthStore = defineStore(
'auth',
() => {
const accessToken = ref('');
const refreshToken = ref('');
const profile = ref<UserProfile | null>(null);

const isLoggedIn = computed(() => !!accessToken.value);

function signIn(login: LoginResponse) {
accessToken.value = login.access_token;
refreshToken.value = login.refresh_token;
profile.value = login.user;
}

function logout() {
accessToken.value = '';
refreshToken.value = '';
profile.value = null;
}

return {
accessToken,
refreshToken,
profile,
logout,
isLoggedIn,
signIn,
};
},
{
persist: {
storage: piniaPluginPersistedstate.cookies(),
},
},
);
This is where the default state is set.
danielroe
danielroeβ€’5w ago
seems like this might be a bug in piniaPluginPersistedstate.cookies
Zampa
Zampaβ€’5w ago
FWIW, I use this plugin and haven't seen this behavior of cookie sharing between users. @RicharDVD Are you using the Nuxt module? It should be declared in your modules config as '@pinia-plugin-persistedstate/nuxt', I set the persist config on a store like so:
persist: {
storage: persistedState.cookiesWithOptions({
maxAge: 31536000,
}),
},
persist: {
storage: persistedState.cookiesWithOptions({
maxAge: 31536000,
}),
},
RicharDVD
RicharDVDOPβ€’5w ago
I do indeed use the nuxt module. It is very hard to reproduce since it happens only happens occasionally. For now, I decided to just use a custom composable where the state is stored with
useCookie()
useCookie()
instead of a pinia store. See if we will run into it again. If so, it must be something completely different. Only thing I could think of was the fact that I access useAuthStore within the onRequest and onResponseError callback. That that might be the reason but not sure
Zampa
Zampaβ€’5w ago
I'm just curious - my config uses persistedstate but yours uses piniaPluginPersistedstate where are you defining piniaPluginPersistedstate ?
RicharDVD
RicharDVDOPβ€’5w ago
I think that is because you are using an older version of the module. Since 4.0.0 it has updated to piniaPluginPersistedstate (https://github.com/prazdevs/pinia-plugin-persistedstate/releases/tag/v4.0.0)
GitHub
Release v4.0.0 Β· prazdevs/pinia-plugin-persistedstate
πŸš€ Enhancements Support excluding paths from persistence with omit option Support autocompletion for dot-notation paths in pick and omit options ⚠️ Rehydrate only picked/omitted paths (when specifi...
Zampa
Zampaβ€’5w ago
hmmm - I don't actually have that in my package.json - I just use: "@pinia-plugin-persistedstate/nuxt": "^1.2.1", which is the most recent version of that module
Zampa
Zampaβ€’5w ago
No description
RicharDVD
RicharDVDOPβ€’5w ago
That is because they moved the module
Zampa
Zampaβ€’5w ago
appears to use 4.1.1
No description
RicharDVD
RicharDVDOPβ€’5w ago
If you look in their docs, they now say that the npm package is "pinia-plugin-persistedstate". https://prazdevs.github.io/pinia-plugin-persistedstate/frameworks/nuxt.html
Usage with Nuxt | Pinia Plugin Persistedstate
Configurable persistence of Pinia stores.
Zampa
Zampaβ€’5w ago
Yeah, odd - the docs seem to use both... well now I am scared to change what's working! πŸ˜› I just tested a few different users in different incognito tabs - no pinia state shared so yeah, something must be getting written and persisted to your store in an unusual way
RicharDVD
RicharDVDOPβ€’5w ago
The thing is, the same for meπŸ˜… It happens only occasionally That is what makes it so hard
Zampa
Zampaβ€’5w ago
Do you have any routeRules? Where your state is being cached?
RicharDVD
RicharDVDOPβ€’5w ago
I do use isr on some routes
Zampa
Zampaβ€’5w ago
if you are caching state on the instance, its possible that if it's leaking that might be the issue Caching has been tricky, as my app (probably like yours) has lots of static content, but then also a user, with login state and a header with their name/avatar/data so if you use isr or swr, it ends up caching the user state Maybe wrapping all user-specific content in ClientOnly would circumvent that, but I'm not sure
RicharDVD
RicharDVDOPβ€’5w ago
I think you might be right. But let's say I make the navbar client only (which uses some auth logic) it would jump into the page which is not so nice. I think I'd have to just remove the cache rules and see if that works out.
manniL
manniLβ€’5w ago
it would. But I'd guess it'd be easier to cache the data in the Nitro API granularly instead of caching the whole SSR'd page
manniL
manniLβ€’5w ago
also outlined that in https://www.youtube.com/watch?v=Zli-u9kxw0w I think - or one of the other videos πŸ˜‚
Alexander Lichter
YouTube
What is BFF?! (With Nuxt, Nitro and h3)
πŸ”— 10$ off for Michael's Nuxt Tips Collection with the following link and code DEJAVUE https://michaelnthiessen.com/nuxt-tips-collection?aff=plY9z * πŸ”— 10% off for vuejs.de Conf with Code LICHTER https://conf.vuejs.de/tickets/?voucher=LICHTER * --- Links and Resources: πŸ”— Code TODO πŸ”— h3 https://h3.unjs.io/ πŸ”— unstorage https://unstorage.unjs.io/ ...
Zampa
Zampaβ€’5w ago
Yeah, I've watched that! I kind of would almost like to see a reverse Server Components option in Nuxt. Where all components are server components, and some select few are Client. Like, a global config to set all components as server components
manniL
manniLβ€’5w ago
I mean, you can do that through .server.vue, also for pages etc.
Zampa
Zampaβ€’5w ago
and use the nuxt-client on the ones that are different Right - but I have like 400+ components πŸ™‚ My use case is that most of the pages I wanted heavily cached but there are "islands" of user-content that should never be cached and I was concerned about how performant setting ALL the components to .server.vue might be but I may experiment with it and see
RicharDVD
RicharDVDOPβ€’5w ago
Thank you guys for helping out. I think I will indeed go with this route since it's an easy solution in my case 1 last thing, I have ISR set to 3600 (so 1 hour). Just for my understanding, under what circumstances will the state be leaked since it's hard for me to reproduce?
manniL
manniLβ€’5w ago
when the cache expires and a logged-in user requests the page
RicharDVD
RicharDVDOPβ€’5w ago
I did confirm that this was indeed the issueπŸ˜ƒ Thank you so much!
danielroe
danielroeβ€’5w ago
you should not call use* functions within the hooks - instead, call them outside the callback, and use them within it.
RicharDVD
RicharDVDOPβ€’5w ago
Thank you for pointing it out. Fixed itπŸ˜ƒ
Want results from more Discord servers?
Add your server