fix(nuxt): show client error if no page matches after validate fails (#18978)

This commit is contained in:
Daniel Roe 2023-02-16 12:56:14 +00:00 committed by GitHub
parent 4b2901bee1
commit 7d0ecb5a96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 77 additions and 7 deletions

View File

@ -1,11 +1,38 @@
import { defineNuxtRouteMiddleware } from '#app/composables/router' import { createError, showError } from '#app/composables/error'
import { callWithNuxt, useNuxtApp } from '#app/nuxt'
import { defineNuxtRouteMiddleware, useRouter } from '#app/composables/router'
export default defineNuxtRouteMiddleware(async (to) => { export default defineNuxtRouteMiddleware(async (to) => {
if (!to.meta?.validate) { return } if (!to.meta?.validate) { return }
const nuxtApp = useNuxtApp()
const router = useRouter()
const result = await Promise.resolve(to.meta.validate(to)) const result = await Promise.resolve(to.meta.validate(to))
if (result === true) { if (result === true) {
return return
} }
if (process.server) {
return result return result
}
const error = createError({
statusCode: 404,
statusMessage: `Page Not Found: ${to.fullPath}`
})
const unsub = router.beforeResolve((final) => {
unsub()
if (final === to) {
const unsub = router.afterEach(async () => {
unsub()
await callWithNuxt(nuxtApp, showError, [error])
// We pretend to have navigated to the invalid route so
// that the user can return to the previous page with
// the back button.
window.history.pushState({}, '', to.fullPath)
})
// We stop the navigation immediately before it resolves
// if there is no other route matching it.
return false
}
})
}) })

View File

@ -98,6 +98,16 @@ describe('pages', () => {
it('validates routes', async () => { it('validates routes', async () => {
const { status } = await fetch('/forbidden') const { status } = await fetch('/forbidden')
expect(status).toEqual(404) expect(status).toEqual(404)
const page = await createPage('/navigate-to-forbidden')
await page.waitForLoadState('networkidle')
await page.getByText('should throw a 404 error').click()
expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"Page Not Found: /forbidden"')
page.goto(url('/navigate-to-forbidden'))
await page.waitForLoadState('networkidle')
await page.getByText('should be caught by catchall').click()
expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"[...slug].vue"')
}) })
it('render 404', async () => { it('render 404', async () => {
@ -107,7 +117,7 @@ describe('pages', () => {
// expect(html).toMatchInlineSnapshot() // expect(html).toMatchInlineSnapshot()
expect(html).toContain('[...slug].vue') expect(html).toContain('[...slug].vue')
expect(html).toContain('404 at not-found') expect(html).toContain('catchall at not-found')
// Middleware still runs after validation: https://github.com/nuxt/nuxt/issues/15650 // Middleware still runs after validation: https://github.com/nuxt/nuxt/issues/15650
expect(html).toContain('Middleware ran: true') expect(html).toContain('Middleware ran: true')
@ -941,7 +951,7 @@ describe.runIf(isDev() && !isWebpack)('vite plugins', () => {
expect(await $fetch('/__nuxt-test')).toBe('vite-plugin with __nuxt prefix') expect(await $fetch('/__nuxt-test')).toBe('vite-plugin with __nuxt prefix')
}) })
it('does not allow direct access to nuxt source folder', async () => { it('does not allow direct access to nuxt source folder', async () => {
expect(await $fetch('/app.config')).toContain('404') expect(await $fetch('/app.config')).toContain('catchall at')
}) })
}) })

15
test/fixtures/basic/error.vue vendored Normal file
View File

@ -0,0 +1,15 @@
<template>
<div>
<div>
<h1>{{ error?.message }}</h1>
This is the error page 😱
</div>
</div>
</template>
<script setup lang="ts">
import type { NuxtError } from '#app'
defineProps({
error: Object as () => NuxtError
})
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div>[...slug].vue</div> <h1>[...slug].vue</h1>
<div>404 at {{ $route.params.slug[0] }}</div> <div>catchall at {{ $route.params.slug[0] }}</div>
<div>Middleware ran: {{ !!($route.meta.override as any)?.includes('extended middleware') }}</div> <div>Middleware ran: {{ !!($route.meta.override as any)?.includes('extended middleware') }}</div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,18 @@
<template>
<div>
<div>navigate-to-forbidden.vue</div>
<NuxtLink to="/forbidden">
should throw a 404 error
</NuxtLink>
<NuxtLink to="/some-404">
should be caught by catchall
</NuxtLink>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['override'],
validate: to => to.path !== '/forbidden'
})
</script>

View File

@ -70,7 +70,7 @@ if (process.env.TEST_ENV !== 'built' && !isWindows) {
it('should detect new routes', async () => { it('should detect new routes', async () => {
const html = await $fetch('/some-404') const html = await $fetch('/some-404')
expect(html).toContain('404 at some-404') expect(html).toContain('catchall at some-404')
// write new page route // write new page route
const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8') const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8')