diff --git a/docs/1.getting-started/6.data-fetching.md b/docs/1.getting-started/6.data-fetching.md index 7800cb16d3f4..69f1da51f221 100644 --- a/docs/1.getting-started/6.data-fetching.md +++ b/docs/1.getting-started/6.data-fetching.md @@ -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 } diff --git a/docs/1.getting-started/8.error-handling.md b/docs/1.getting-started/8.error-handling.md index e340967ec113..eeb004d2f17e 100644 --- a/docs/1.getting-started/8.error-handling.md +++ b/docs/1.getting-started/8.error-handling.md @@ -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] diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index 69ebb5e523d6..ea7892398419 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -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(url, { diff --git a/packages/nuxt/src/app/composables/cookie.ts b/packages/nuxt/src/app/composables/cookie.ts index a578b85e4975..a05773cd7941 100644 --- a/packages/nuxt/src/app/composables/cookie.ts +++ b/packages/nuxt/src/app/composables/cookie.ts @@ -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 (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 @@ -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)) } } diff --git a/packages/nuxt/src/app/composables/error.ts b/packages/nuxt/src/app/composables/error.ts index 3d1820f139c9..b23fcd32ee57 100644 --- a/packages/nuxt/src/app/composables/error.ts +++ b/packages/nuxt/src/app/composables/error.ts @@ -13,8 +13,10 @@ export const showError = (_err: string | Error | Partial) => { 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 diff --git a/packages/nuxt/src/components/runtime/server-component.ts b/packages/nuxt/src/components/runtime/server-component.ts index bef75caf4a3f..cdd9e69d4764 100644 --- a/packages/nuxt/src/components/runtime/server-component.ts +++ b/packages/nuxt/src/components/runtime/server-component.ts @@ -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(url, { diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 0ec0d5d4be1b..f997c85acd83 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -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 { }) 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') }) diff --git a/test/fixtures/basic/plugins/cookie.ts b/test/fixtures/basic/plugins/cookie.ts new file mode 100644 index 000000000000..8e969548c996 --- /dev/null +++ b/test/fixtures/basic/plugins/cookie.ts @@ -0,0 +1,3 @@ +export default defineNuxtPlugin(() => { + useCookie('set-in-plugin').value = 'true' +})