From f0075f5292a8bc217747acad7b3443c247d0ff76 Mon Sep 17 00:00:00 2001 From: wattanx Date: Sun, 11 Oct 2026 23:47:28 +0900 Subject: [PATCH] fix: stop middleware chain when navigation is redirected or aborted --- packages/bridge/src/runtime/app.plugin.mjs | 2 +- .../bridge/src/runtime/composables/router.ts | 20 ++++++++++++++- playground/middleware/break-chain.ts | 6 +++++ playground/middleware/unreachable.ts | 3 +++ playground/pages/middleware-chain/break.vue | 9 +++++++ playground/pages/middleware-chain/index.vue | 22 ++++++++++++++++ test/bridge.test.ts | 25 +++++++++++++++++++ 7 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 playground/middleware/break-chain.ts create mode 100644 playground/middleware/unreachable.ts create mode 100644 playground/pages/middleware-chain/break.vue create mode 100644 playground/pages/middleware-chain/index.vue diff --git a/packages/bridge/src/runtime/app.plugin.mjs b/packages/bridge/src/runtime/app.plugin.mjs index 6ee6eae07..87f856812 100644 --- a/packages/bridge/src/runtime/app.plugin.mjs +++ b/packages/bridge/src/runtime/app.plugin.mjs @@ -73,7 +73,7 @@ export default async (ctx, inject) => { nuxtApp._processingMiddleware = true for (const middleware of nuxtApp._middleware.global) { - const result = await middleware({ ...ctx, route: to, from }) + const result = await middleware({ ...ctx, route: to, from, _isGlobalMiddleware: true }) if (result || result === false) { return next(result) } } diff --git a/packages/bridge/src/runtime/composables/router.ts b/packages/bridge/src/runtime/composables/router.ts index 0a7d452cf..c64b9cd9e 100644 --- a/packages/bridge/src/runtime/composables/router.ts +++ b/packages/bridge/src/runtime/composables/router.ts @@ -55,7 +55,7 @@ export interface AddRouteMiddlewareOptions { } /** internal */ -function convertToLegacyMiddleware (middleware) { +function convertToLegacyMiddleware (middleware: RouteMiddleware) { return async (ctx: any) => { // because the middleware is executed before the plugin ctx.$_nuxtApp._processingMiddleware = true @@ -64,6 +64,24 @@ function convertToLegacyMiddleware (middleware) { if (result instanceof Error) { return ctx.error(result) } + if (result === false) { + // global middleware passes the result to `next` within `router.beforeEach` + if (ctx._isGlobalMiddleware) { + return result + } + // Nuxt 2 `middlewareSeries` stops calling the remaining middleware when `_redirected` is set + ctx._redirected = true + if (process.server && ctx.ssrContext['~renderResponse']) { + ctx.ssrContext.redirected = true + return + } + if (process.server || (!ctx.$_nuxtApp.payload.serverRendered && ctx.$_nuxtApp.isHydrating)) { + return ctx.error(createError({ statusCode: 404, statusMessage: `Page Not Found: ${ctx.route.fullPath}` })) + } + ctx.next(false) + ctx.$_nuxtApp.vue2App.$loading?.finish?.() + return + } if (result) { return ctx.redirect(result) } diff --git a/playground/middleware/break-chain.ts b/playground/middleware/break-chain.ts new file mode 100644 index 000000000..0be6b8181 --- /dev/null +++ b/playground/middleware/break-chain.ts @@ -0,0 +1,6 @@ +export default defineNuxtRouteMiddleware((to) => { + if ('abort' in to.query) { + return abortNavigation() + } + return navigateTo('/navigation-target') +}) diff --git a/playground/middleware/unreachable.ts b/playground/middleware/unreachable.ts new file mode 100644 index 000000000..663ee9ea9 --- /dev/null +++ b/playground/middleware/unreachable.ts @@ -0,0 +1,3 @@ +export default defineNuxtRouteMiddleware(() => { + throw new Error('This middleware should not be called after the navigation has been redirected or aborted') +}) diff --git a/playground/pages/middleware-chain/break.vue b/playground/pages/middleware-chain/break.vue new file mode 100644 index 000000000..8bac92023 --- /dev/null +++ b/playground/pages/middleware-chain/break.vue @@ -0,0 +1,9 @@ + + + diff --git a/playground/pages/middleware-chain/index.vue b/playground/pages/middleware-chain/index.vue new file mode 100644 index 000000000..b388fdd64 --- /dev/null +++ b/playground/pages/middleware-chain/index.vue @@ -0,0 +1,22 @@ + + + diff --git a/test/bridge.test.ts b/test/bridge.test.ts index ad1e12094..3d8c90c4e 100644 --- a/test/bridge.test.ts +++ b/test/bridge.test.ts @@ -306,6 +306,31 @@ describe('middleware', () => { expect(html).toContain('Navigated successfully') }) + it('should not call the remaining middleware after navigateTo on server-side', async () => { + const res = await fetch('/middleware-chain/break', { redirect: 'manual' }) + + expect(res.status).toEqual(302) + expect(res.headers.get('location')).toEqual('/navigation-target') + }) + + it('should not call the remaining middleware after abortNavigation on server-side', async () => { + const res = await fetch('/middleware-chain/break?abort', { redirect: 'manual' }) + + expect(res.status).toEqual(404) + }) + + it('should abort navigation without calling the remaining middleware on client-side', async () => { + const { page, pageErrors } = await renderPage('/middleware-chain') + await page.getByRole('button', { name: 'navigate' }).click() + await page.getByText('navigation aborted').waitFor() + + expect(new URL(page.url()).pathname).toEqual('/middleware-chain') + expect(await page.textContent('body')).toContain('middleware-chain/index.vue') + expect(pageErrors).toEqual([]) + + await page.close() + }) + // if `no-resolve`, an error occurs at `useRoute`. it.skipIf(isWebpack && isNoResolve)('should be output with target as to and origin as from', async () => { const { page, consoleLogs } = await renderPage('/')