manchenkoff / nuxt-auth-sanctum

Nuxt module for Laravel Sanctum authentication
https://manchenkoff.gitbook.io/nuxt-auth-sanctum/
MIT License
168 stars 21 forks source link

[Bug] `useAsyncData` fails with multiple `useSanctumClient` calls #39

Closed SDIjeremy closed 7 months ago

SDIjeremy commented 8 months ago

I'm not really sure the best way to make a shareable reproduction for this, but I'm running into a very very strange problem, and it seems related to the useSanctumClient...

Within useAsyncData, if you use any other await then I get the below error. Even if I put two fetchSanctums in different places within the same function, it seems to have problems. This isn't the case when using $fetch within useAsyncData.

[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug.

https://gist.github.com/SDIjeremy/e86c2d33483aff7f89ddc85af6a9f1fc

manchenkoff commented 8 months ago

Hey @SDIjeremy, are you talking about this particular method from your gist?

const { data, error }: { data: Ref, error: Ref } = await useAsyncData(`chatsBroken`, async () => {
    await new Promise(resolve => { setTimeout(resolve, 5); });
    // Doing any other async action breaks this ....
    return await fetchSanctum(`https://jsonplaceholder.typicode.com/posts`);
});
SDIjeremy commented 8 months ago

Hey @SDIjeremy, are you talking about this particular method from your gist?

const { data, error }: { data: Ref, error: Ref } = await useAsyncData(`chatsBroken`, async () => {
    await new Promise(resolve => { setTimeout(resolve, 5); });
    // Doing any other async action breaks this ....
    return await fetchSanctum(`https://jsonplaceholder.typicode.com/posts`);
});

Yes! For example I have to first fetch an ID/Slug from the Laravel API then pass that ID/Slug in the following request for all the posts. But no matter how I do it (with fetchSanctum) I get that composable error.

The weirdest thing also, is that if I make that first request I need somewhere else (in a Middleware) and have that data then it works fine.

SDIjeremy commented 8 months ago

Working:

const { data, error }: { data: Ref, error: Ref } = await useAsyncData(`chats`, async () => {
    const [ post ] = await $fetch('https://jsonplaceholder.typicode.com/posts');
    return await $fetch(`https://jsonplaceholder.typicode.com/comments?postId=${post.id}`);
});

Not Working:

const sanctumFetch = useSanctumClient();
const { data, error }: { data: Ref, error: Ref } = await useAsyncData(`chats`, async () => {
    const [ post ] = await sanctumFetch('https://jsonplaceholder.typicode.com/posts');
    return await sanctumFetch(`https://jsonplaceholder.typicode.com/comments?postId=${post.id}`);
});
SDIjeremy commented 8 months ago

In these cases it is ~not throwing the error about composables, but instead~ not returning any date in the case of useSanctumClient.

Edit: if you put a try catch around the second sanctumFetch, it will log the the error about composables.

Working

<script setup lang="ts">
definePageMeta({
    middleware: [
        'sanctum:auth',
    ]
})

const { data, error }: { data: Ref, error: Ref } = await useAsyncData(`chats`, async () => {
    const [post] = await $fetch('https://jsonplaceholder.typicode.com/posts');
    console.log('post', post);
    const comments = await $fetch(`https://jsonplaceholder.typicode.com/comments?postId=${post.id}`);
    console.log('comments', comments);
    return comments;
});
console.log(data.value);
</script>

<template>
    <section>
        <div class="container">
            <h1>Profile</h1>
            <pre>{{ data }}</pre>
        </div>
    </section>
</template>

Not Working

<script setup lang="ts">
definePageMeta({
    middleware: [
        'sanctum:auth',
    ]
})

const sanctumFetch = useSanctumClient();
const { data, error }: { data: Ref, error: Ref } = await useAsyncData(`chats`, async () => {
    const [post] = await sanctumFetch('https://jsonplaceholder.typicode.com/posts');
    console.log('post', post); // this outputs
    const comments = await sanctumFetch(`https://jsonplaceholder.typicode.com/comments?postId=${post.id}`);
    console.log('comments', comments); // this is null, never runs
    return comments;
});
console.log(data.value);
</script>

<template>
    <section>
        <div class="container">
            <h1>Profile</h1>
            <pre>{{ data }}</pre>
        </div>
    </section>
</template>
SDIjeremy commented 8 months ago

I'm thinking this is an SSR issue when using multiple sanctumFetch. Because when you navigate from one page to the page above, the data displays properly. But if you refresh the page, then there is no data in the case when using sanctumFetch, but it is working with $fetch.

Edit: even more specifically an issue when using useAsyncData as both codes above work outside of it.

Edit2: the composable error only seems to happen from within the Pinia action when there is more than one sanctumFetch inside the function, wether it is encased in a useAsyncData or not.

manchenkoff commented 8 months ago

thanks a lot @SDIjeremy for such a detailed reply 👍 it looks like a rare case to me, but it might be the same problem we can face inside of the module when we have multiple calls on the same Nuxt app, here are more details about it. I will try to take a closer look on the weekend.

SDIjeremy commented 8 months ago

I think that might be the wrong link @manchenkoff 🙃

For now I will stick with ssr: false until we can resolve this.

manchenkoff commented 8 months ago

I think that might be the wrong link @manchenkoff 🙃

Ooops 😄 updated

For now I will stick with ssr: false until we can resolve this.

Okay, I will let you know once I find the solution

SDIjeremy commented 8 months ago

any luck on this? would love to have SSR working again. I think maybe this could be more of a Nuxt issue?

manchenkoff commented 7 months ago

Hey @SDIjeremy, nope, no luck at all, the only thing I confirmed is that it doesn't happen with multiple calls if you use Promise.all() with several useAsyncData inside. Multiple calls of sanctum client w/o this composable also don't affect the behavior but when it comes to useAsyncData with multiple calls inside - it fails with this error. I believe this is some sort of Nuxt internal scope magic 😄

Right now I am unsure about a possible solution, but I will check the sources of Nuxt composable to find a reason and will try to update this issue once I have something.

jjjrmy commented 7 months ago

Hey @SDIjeremy, nope, no luck at all, the only thing I confirmed is that it doesn't happen with multiple calls if you use Promise.all() with several useAsyncData inside. Multiple calls of sanctum client w/o this composable also don't affect the behavior but when it comes to useAsyncData with multiple calls inside - it fails with this error. I believe this is some sort of Nuxt internal scope magic 😄

Right now I am unsure about a possible solution, but I will check the sources of Nuxt composable to find a reason and will try to update this issue once I have something.

Can you share the example for this so I can try it locally?

manchenkoff commented 7 months ago

Can you share the example for this so I can try it locally?

Sure thing, I used something like this

const sanctumFetch = useSanctumClient();

async function initStoreOne() {
    const { data, error } = await useAsyncData(() => sanctumFetch('/api/one'));
    // process data and store results somewhere
}

async function initStoreTwo() {
    const { data, error } = await useAsyncData(() => sanctumFetch('/api/two'));
    // process data and store results somewhere
}

const tasks = [initStoreOne, initStoreTwo];
await Promise.all(tasks);

The code example was changed a bit because I used it for two separate Pinia store initialization functions that use useSanctumClient to load data during the first app run.

But when it comes to several calls inside of one useAsyncData, then it generates a warning, like in the examples above

manchenkoff commented 7 months ago

Hey @SDIjeremy, I have a small update here 😄

So, after some investigation, I can confirm that the problem is related to the Nuxt app instance which is reset after the first call inside of useAsyncData and since my plugin client depends on it to collect cookies for sending to the API - it fails. I also noticed that it fails only on SSR mode while CSR can do everything w/o any issue 🤔

Here you can find more details about this instance reset operation - Nuxt docs.

Anyway, it looks like they have a solution that I had a chance to test and didn't get any error in both CSR/SSR modes. Here it is - asyncContext.

I've used this code to test it

const response = await useAsyncData(async () => {
    try {
        const user1 = await client('/api/user');
        console.log('User loaded [1/2]', user1);
        const user2 = await client('/api/user');
        console.log('User loaded [2/2]', user2);

        return user2;
    } catch (e) {
        console.error(e);
    }
});

As a result, with asyncContext I got no errors while w/o it Nuxt still shows that warning.

I'm closing this for now, feel free to reopen if there are some issues!

SDIjeremy commented 7 months ago

@manchenkoff amazing research!! I will try it myself!!