nuxt
nuxt copied to clipboard
SSR & Await (directus example)
Environment
- Operating System:
Darwin
- Node Version:
v16.17.1
- Nuxt Version:
3.1.0
- Nitro Version:
2.0.0
- Package Manager:
[email protected]
- Builder:
vite
- User Config:
-
- Runtime Modules:
-
- Build Modules:
-
Reproduction
(https://github.com/bryantgillespie/nuxt3-directus-starter) Create directus plugin
import { BaseStorage, Directus } from '@directus/sdk'
import { useAuth } from '~~/store/auth'
// Make sure you review the Directus SDK documentation for more information
// https://docs.directus.io/reference/sdk.html
export default defineNuxtPlugin(async (nuxtApp) => {
const { directusUrl } = useRuntimeConfig()
// Create a new storage class to use with the SDK
// Needed for the SSR to play nice with the SDK
class CookieStorage extends BaseStorage {
deletedKeys = new Set<string>()
get(key: string) {
if (this.deletedKeys.has(key)) return null
const cookie = useCookie(key)
return cookie.value
}
set(key: string, value: string) {
this.deletedKeys.delete(key)
const cookie = useCookie(key)
return (cookie.value = value)
}
delete(key: string) {
this.deletedKeys.add(key)
const cookie = useCookie(key)
return (cookie.value = null)
}
}
// Create a new instance of the SDK
const directus = new Directus(directusUrl, {
storage: new CookieStorage(),
auth: {
mode: 'json',
},
})
// Inject the SDK into the Nuxt app
nuxtApp.provide('directus', directus)
// We're calling the useAuth composable here because we need to define Directus as a plugin first
const auth = useAuth()
const token = await directus.auth.token
const side = process.server ? 'server' : 'client'
// If there's a token but we don't have a user, fetch the user
if (!auth.isLoggedIn && token) {
console.log('Token found, fetching user from ' + side)
console.log('Token is', token)
try {
await auth.getUser()
console.log('User fetched succeessfully from ' + side)
} catch (e) {
console.log('Failed to fetch user from ' + side, e.message)
}
}
// If the user is logged in but there's no token, reset the auth store {
if (auth.isLoggedIn && !token) {
console.log('Token not found, resetting auth store from ' + side)
auth.$reset()
}
})
Create page with
<script setup>
const { $directus } = useNuxtApp()
const usr = await $directus.users.me.read()
</script>
Describe the bug
When SSR enabled:
400
Bad Request
at createError (./node_modules/h3/dist/index.mjs:48:15)
at Object.handler (./.nuxt/dev/index.mjs:740:15)
at Object.handler (./node_modules/h3/dist/index.mjs:723:31)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async toNodeHandle (./node_modules/h3/dist/index.mjs:798:7)
at async Object.ufetch [as localFetch] (./node_modules/unenv/runtime/fetch/index.mjs:9:17)
at async Object.errorhandler [as onError] (./.nuxt/dev/index.mjs:453:30)
at async Server.toNodeHandle (./node_modules/h3/dist/index.mjs:805:9)
When SSR disabled: it works fine!
Additional context
No response
Logs
Only 400 - bad request
When i used try - catch it start work, but in console i have
TransportError: nuxt instance unavailable
at Transport.request /.output/server/node_modules/@directus/sdk/dist/sdk.cjs.js:730:19)
at runMicrotasks (
parent: Error: nuxt instance unavailable at useNuxtApp /.output/server/chunks/app/server.mjs:152:13)
https://stackblitz.com/edit/github-cfogyu
Hey,
Just out of curiosity, why are you not using the directus module for nuxt?
I tried this plugin, but with all due respect to the work, it does not deserve attention
I create custom endpoints using directus and i want to use it in nuxt Problem is in any sdk where we have async function and setup as plugin
What about wrapping your directus call with a onMounted
or inside useAsyncData
like following?
<template><div>test</div></template>
<script setup>
const { $directus } = useNuxtApp();
console.log($directus);
onMounted(async () => {
const usr = await $directus.users.me.read();
console.log(usr);
});
</script>
In my case it seems to be working correctly and the issue with the instance is gone
What about wrapping your directus call with a
onMounted
or insideuseAsyncData
like following?<template><div>test</div></template> <script setup> const { $directus } = useNuxtApp(); console.log($directus); onMounted(async () => { const usr = await $directus.users.me.read(); console.log(usr); }); </script>
In my case it seems to be working correctly and the issue with the instance is gone
It's not SRR, it's just a simple example of my request, in real project based on data should build whole page and it should be with ssr. When we use onMounted -> its client side rendering
Hmm, it seems that it does not work on either top lever await nor on useAsyncData.
When I replaced your call for geting users with just a simple console log to see what is stored as a $directus
in nuxtApp, the error is no more and I can see the following code in the console:
$directus Directus { 16:09:44
_url: undefined,
_options: {
storage: CookieStorage { prefix: '', deletedKeys: Set(0) {} },
auth: { mode: 'json' }
},
_items: {},
_singletons: {},
_storage: CookieStorage { prefix: '', deletedKeys: Set(0) {} },
_transport: Transport {
config: { url: undefined, beforeRequest: [AsyncFunction: beforeRequest] },
axios: [Function: wrap] {
request: [Function: wrap],
getUri: [Function: wrap],
delete: [Function: wrap],
get: [Function: wrap],
head: [Function: wrap],
options: [Function: wrap],
post: [Function: wrap],
postForm: [Function: wrap],
put: [Function: wrap],
putForm: [Function: wrap],
patch: [Function: wrap],
patchForm: [Function: wrap],
defaults: [Object],
interceptors: [Object],
create: [Function: create]
},
beforeRequest: [AsyncFunction: beforeRequest] 16:09:34
},
_auth: Auth {
mode: 'json',
autoRefresh: true,
msRefreshBeforeExpires: 30000,
staticToken: '',
_transport: Transport {
config: [Object],
axios: [Function],
beforeRequest: [AsyncFunction: beforeRequest]
},
_storage: CookieStorage { prefix: '', deletedKeys: Set(0) {} }
}
}
Are you sure you should be accessing this directus client like this const usr = await $directus.users.me.read();
?
The error with the nuxt instance seems to be completely unrelated to this as this is $directus variable is just a client, it does not have users in it.
Unless I am doing something wrong.
I have the same issue:
- Operating System: `Windows_NT`
- Node Version: `v18.13.0`
- Nuxt Version: `3.1.1`
- Nitro Version: `2.1.0`
- Package Manager: `[email protected]`
- Builder: `vite`
- User Config: `ssr`, `modules`, `strapi`
- Runtime Modules: `@nuxtjs/[email protected]` <- not active in my example
- Build Modules: `-`
With the following code I am able to reproduce the issue, and all I am changing (and have) in the Nuxt config is ssr
. With ssr: false
the code works, with ssr: true
the code does not work on first load, but it does work if I make changes to the code and it hot reloads.
<template>
<div>
<h1>useAsyncData</h1>
<pre>
{{ data }}
</pre>
<h1>useFetch</h1>
<pre>
{{ data2 }}
</pre>
</div>
</template>
<script setup>
const { data } = await useAsyncData('test', () => useFetch('http://localhost:1337/api/articles'));
const data2 = await useFetch('http://localhost:1337/api/articles');
</script>
The browser output:
useAsyncData
{
"data": null,
"pending": false,
"error": {
"name": "FetchError"
},
"execute": {},
"refresh": {}
}
useFetch
{
"data": null,
"pending": false,
"error": "Error: fetch failed ()"
}
The api is Strapi, and I have the same issue if I use the Strapi module.
It was a lot of fiddling and trying things out... for below to work you have to be on the same domain and have ssl enabled. It basically always comes down to the cookie settings.
Directus: api.domain.com
App: domain.com
Cookie: .domain.com
Dropping my working plugins/directus.js
and composables. (sorry for the long paste)
below code only puts the refresh token in a cookie, all other short lived token info is going in the state.
This is far from perfect, but better than using the directus module, which somehow does not work for the more advanced usecases.
import { BaseStorage, Directus } from '@directus/sdk'
import { useCookie } from '#imports'
export default defineNuxtPlugin(async (nuxtApp) => {
const { directusUrl, cookieDomain } = useRuntimeConfig()
const auth = useAuth()
const user = useUser()
const auth_refresh_token = useCookie('directus_refresh_token', {
maxAge: 3600 * 24 * 7,
// httpOnly: true,
secure: true,
sameSite: 'None',
domain: cookieDomain, // .maindomain.com (put directuson a subdomain like api.maindomain.com)
})
const auth_token = useState('auth_token')
const auth_expires = useState('auth_expires')
const auth_expires_at = useState('auth_expires_at')
const cookies = {
auth_token: auth_token,
auth_expires: auth_expires,
auth_expires_at: auth_expires_at,
auth_refresh_token: auth_refresh_token,
}
class CookieStorage extends BaseStorage {
deletedKeys = new Set()
get(key) {
if (this.deletedKeys.has(key)) return null
const cookie = cookies[key]
return cookie.value
}
set(key, value) {
this.deletedKeys.delete(key)
const cookie = cookies[key]
return (cookie.value = value)
}
delete(key) {
this.deletedKeys.add(key)
const cookie = cookies[key]
return (cookie.value = null)
}
}
// Create a new instance of the SDK
const directus = new Directus(directusUrl, {
storage: new CookieStorage(),
auth: {
mode: 'json',
},
})
// Inject the SDK into the Nuxt app
nuxtApp.provide('directus', directus)
let token = await directus.auth.token
// console.log(token)
if (process.server && auth_refresh_token.value && !token) {
try {
const x = await directus.auth.refresh()
} catch (e) {
//failed
console.log(e, auth_refresh_token.value)
}
}
const side = process.server ? 'server' : 'client'
token = await directus.auth.token
if (auth.isAuthenticated.value === false && token !== undefined) {
try {
if (process.server) {
await user.init(directus)
auth.isAuthenticated.value = true
}
} catch (e) {
console.log(e)
// failed?
}
}
// If the user is logged in but there's no token, reset the auth store {
if (auth.isAuthenticated.value && !token) {
console.error('Token not found, resetting auth store from ' + side)
}
})
composables/useUser.js
export const useUser = () => {
const { $directus } = useNuxtApp()
const user = useState('directus.user', () => ({}))
const init = async (directus) => {
const directusUser = directus ? directus.users : $directus.users
const value = await directusUser.me.read({
fields: ['*'],
})
user.value = value
}
return {
state: user,
init,
setData: (value) => {
user.value = value
},
data: computed(() => user.value),
reset: () => (user.value = {}),
}
}
composables/useAuth.js
export const useAuth = () => {
const nuxtApp = useNuxtApp()
const { $directus } = useNuxtApp()
const route = useRoute()
const userState = useUser()
const isAuthenticated = useState('auth.isAuthenticated', () => false)
const stateErrors = useState('auth.errors', () => [])
const login = async (email, password) => {
stateErrors.value = []
await $directus.auth.login({
email: email,
password: password,
})
await $directus.auth.refresh()
// fetch user data
await userState.init()
isAuthenticated.value = true
}
const loginOnSubmit = async ($event) => {
const email = $event.srcElement.email.value
const password = $event.srcElement.password.value
try {
await login(email, password)
return true
} catch (error) {
if (error.response) {
stateErrors.value = error.response.errors
return error.response.errors
}
return false
}
}
const logout = async () => {
await $directus.auth.logout()
isAuthenticated.value = false
if (nuxtApp.ssrContext === undefined) {
window.location = route.path
}
}
return {
isAuthenticated,
login,
logout
}
}
I am currently looking into sidebase.io/nuxt-auth/ and https://gist.github.com/madsh93/b573b3d8f070e62eaebc5c53ae34e2cc ... just a small thing i need to figure out with refresh tokens not being updated before it's usable. The moment I got it working I wil create a Nuxt "layer". Also looking into using Directus SDK without axios by replacing it with $fetch or use https://github.com/jacoborus/directus-lite-sdk.
I think the answer was given to the question. I am closing the issue but if there is still something that should be added, I will reopen the issue, no worries :)