Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from May 1, 2023
Merged
4 changes: 2 additions & 2 deletions docs/1.getting-started/6.data-fetching.md
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions docs/1.getting-started/8.error-handling.md
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt/src/app/components/nuxt-island.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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, {
Expand Down
9 changes: 4 additions & 5 deletions packages/nuxt/src/app/composables/cookie.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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))
}
}
4 changes: 3 additions & 1 deletion packages/nuxt/src/app/composables/error.ts
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions 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'
Expand Down Expand Up @@ -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, {
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt/src/core/runtime/nitro/renderer.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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))
}
Expand Down
14 changes: 10 additions & 4 deletions test/basic.test.ts
Expand Up @@ -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')
Expand All @@ -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()
Expand Down Expand Up @@ -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')
})

Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/basic/plugins/cookie.ts
@@ -0,0 +1,3 @@
export default defineNuxtPlugin(() => {
useCookie('set-in-plugin').value = 'true'
})