fix(nuxt): ensure useError is called with nuxt app context (#20585)

This commit is contained in:
Daniel Roe 2023-05-01 23:55:24 +01:00 committed by GitHub
parent 675445f98a
commit 16bf228437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 32 additions and 18 deletions

View File

@ -314,13 +314,13 @@ Be very careful before proxying headers to an external API and just include head
If you want to pass on/proxy cookies in the other direction, from an internal request back to the client, you will need to handle this yourself.
```ts [composables/fetch.ts]
import { appendHeader, H3Event } from 'h3'
import { appendResponseHeader, H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
const res = await $fetch.raw(url)
const cookies = (res.headers.get('set-cookie') || '').split(',')
for (const cookie of cookies) {
appendHeader(event, 'set-cookie', cookie)
appendResponseHeader(event, 'set-cookie', cookie)
}
return res._data
}

View File

@ -67,6 +67,10 @@ When you are ready to remove the error page, you can call the `clearError` helpe
Make sure to check before using anything dependent on Nuxt plugins, such as `$route` or `useRouter`, as if a plugin threw an error, then it won't be re-run until you clear the error.
::
::alert{type="warning"}
If you are running on Node 16 and you set any cookies when rendering your error page, they will [overwrite cookies previously set](https://github.com/nuxt/nuxt/pull/20585). We recommend using a newer version of Node as Node 16 will reach end-of-life in September 2023.
::
### Example
```vue [error.vue]

View File

@ -2,7 +2,7 @@ import type { RendererNode } from 'vue'
import { computed, createStaticVNode, defineComponent, getCurrentInstance, h, ref, watch } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendHeader } from 'h3'
import { appendResponseHeader } from 'h3'
import { useHead } from '@unhead/vue'
// eslint-disable-next-line import/no-restricted-paths
@ -42,7 +42,7 @@ export default defineComponent({
const url = `/__nuxt_island/${props.name}:${hashId.value}`
if (process.server && process.env.prerender) {
// Hint to Nitro to prerender the island component
appendHeader(event, 'x-nitro-prerender', url)
appendResponseHeader(event, 'x-nitro-prerender', url)
}
// TODO: Validate response
return $fetch<NuxtIslandResponse>(url, {

View File

@ -2,7 +2,7 @@ import type { Ref } from 'vue'
import { ref, watch } from 'vue'
import type { CookieParseOptions, CookieSerializeOptions } from 'cookie-es'
import { parse, serialize } from 'cookie-es'
import { appendHeader } from 'h3'
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
import destr from 'destr'
import { isEqual } from 'ohash'
@ -48,11 +48,10 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
}
}
const unhook = nuxtApp.hooks.hookOnce('app:rendered', writeFinalCookieValue)
const writeAndUnhook = () => {
nuxtApp.hooks.hookOnce('app:error', () => {
unhook() // don't write cookie subsequently when app:rendered is called
return writeFinalCookieValue()
}
nuxtApp.hooks.hookOnce('app:error', writeAndUnhook)
})
}
return cookie as CookieRef<T>
@ -82,6 +81,6 @@ function writeClientCookie (name: string, value: any, opts: CookieSerializeOptio
function writeServerCookie (event: H3Event, name: string, value: any, opts: CookieSerializeOptions = {}) {
if (event) {
// TODO: Try to smart join with existing Set-Cookie headers
appendHeader(event, 'Set-Cookie', serializeCookie(name, value, opts))
appendResponseHeader(event, 'Set-Cookie', serializeCookie(name, value, opts))
}
}

View File

@ -13,8 +13,10 @@ export const showError = (_err: string | Error | Partial<NuxtError>) => {
try {
const nuxtApp = useNuxtApp()
nuxtApp.callHook('app:error', err)
const error = useError()
if (process.client) {
nuxtApp.hooks.callHook('app:error', err)
}
error.value = error.value || err
} catch {
throw err

View File

@ -1,7 +1,7 @@
import { Fragment, computed, createStaticVNode, createVNode, defineComponent, h, ref, watch } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendHeader } from 'h3'
import { appendResponseHeader } from 'h3'
import { useHead } from '@unhead/vue'
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
@ -51,7 +51,7 @@ const NuxtServerComponent = defineComponent({
const url = `/__nuxt_island/${props.name}:${hashId.value}`
if (process.server && process.env.prerender) {
// Hint to Nitro to prerender the island component
appendHeader(event, 'x-nitro-prerender', url)
appendResponseHeader(event, 'x-nitro-prerender', url)
}
// TODO: Validate response
return $fetch<NuxtIslandResponse>(url, {

View File

@ -2,7 +2,7 @@ import { createRenderer, renderResourceHeaders } from 'vue-bundle-renderer/runti
import type { RenderResponse } from 'nitropack'
import type { Manifest } from 'vite'
import type { H3Event } from 'h3'
import { appendHeader, createError, getQuery, readBody, writeEarlyHints } from 'h3'
import { appendResponseHeader, createError, getQuery, readBody, writeEarlyHints } from 'h3'
import devalue from '@nuxt/devalue'
import { stringify, uneval } from 'devalue'
import destr from 'destr'
@ -275,7 +275,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
if (_PAYLOAD_EXTRACTION) {
// Hint nitro to prerender payload for this route
appendHeader(event, 'x-nitro-prerender', joinURL(url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js'))
appendResponseHeader(event, 'x-nitro-prerender', joinURL(url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js'))
// Use same ssr context to generate payload for this route
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
}

View File

@ -114,8 +114,9 @@ describe('pages', () => {
})
it('validates routes', async () => {
const { status } = await fetch('/forbidden')
const { status, headers } = await fetch('/forbidden')
expect(status).toEqual(404)
expect(headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/')
const page = await createPage('/navigate-to-forbidden')
await page.waitForLoadState('networkidle')
@ -135,8 +136,11 @@ describe('pages', () => {
expect(status).toEqual(500)
})
it('render 404', async () => {
const html = await $fetch('/not-found')
it('render catchall page', async () => {
const res = await fetch('/not-found')
expect(res.status).toEqual(200)
const html = await res.text()
// Snapshot
// expect(html).toMatchInlineSnapshot()
@ -578,7 +582,9 @@ describe('errors', () => {
it('should render a HTML error page', async () => {
const res = await fetch('/error')
expect(res.headers.get('Set-Cookie')).toBe('some-error=was%20set; Path=/')
expect(res.headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/')
// TODO: enable when we update test to node v16
// expect(res.headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/, some-error=was%20set; Path=/')
expect(await res.text()).toContain('This is a custom error')
})

3
test/fixtures/basic/plugins/cookie.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export default defineNuxtPlugin(() => {
useCookie('set-in-plugin').value = 'true'
})