From c70a58ae482cc9086a1f0cba6f04ca9399ffcc32 Mon Sep 17 00:00:00 2001 From: kingmakeruix <300409226+kingmakeruix@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:23:31 +0300 Subject: [PATCH 1/5] fix(server): handle file watcher errors without crashing --- .../src/node/__tests__/plugins/hooks.spec.ts | 28 +++++++++++++++++++ packages/vite/src/node/server/index.ts | 7 +++++ 2 files changed, 35 insertions(+) diff --git a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts index d532b2cc57afa2..e560ed028f287d 100644 --- a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts @@ -445,6 +445,34 @@ describe('watcher add/unlink error handling', () => { expect(logError).toHaveBeenCalled() expect(logError).toHaveBeenCalledWith(error) }) + + test("'error' event logs warning instead of crashing the server", async () => { + const { promise, resolve } = promiseWithResolvers() + const error = Object.assign(new Error('watch failed'), { code: 'EBUSY' }) + + const logWarn = vi.fn() + const logger = createLogger('error') + logger.warn = (...args) => { + logWarn(...args) + resolve() + } + + const server = await createServerWithPlugin( + { + name: 'test', + }, + logger, + ) + + expect(server.watcher.listenerCount('error')).toBeGreaterThan(0) + expect(() => { + server.watcher.emit('error', error) + }).not.toThrow() + + await promise + expect(logWarn).toHaveBeenCalled() + expect(logWarn.mock.calls[0][0]).toContain('watch failed') + }) }) describe('closeServer hook', () => { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index c93bbb6eb1e2d5..2a0f8561975bb5 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -967,6 +967,13 @@ export async function _createServer( watcher.on('unlink', (file) => { onFileAddUnlink(file, true).catch((e) => server.config.logger.error(e)) }) + watcher.on('error', (error: Error) => { + // A single unwatchable path (e.g. EBUSY on a briefly locked file) + // must not crash the dev server with an uncaught 'error' event. + server.config.logger.warn( + colors.yellow(`file watcher error: ${error.message}`), + ) + }) if (!middlewareMode && httpServer) { httpServer.once('listening', () => { From 3413c32781ea34298d05bde5b64508829011660e Mon Sep 17 00:00:00 2001 From: kingmakeruix <300409226+kingmakeruix@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:08:43 +0300 Subject: [PATCH 2/5] test(server): cover watcher error during startup --- .../src/node/__tests__/plugins/hooks.spec.ts | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts index e560ed028f287d..cea40eeda6d0a8 100644 --- a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts @@ -446,32 +446,26 @@ describe('watcher add/unlink error handling', () => { expect(logError).toHaveBeenCalledWith(error) }) - test("'error' event logs warning instead of crashing the server", async () => { - const { promise, resolve } = promiseWithResolvers() + test("'error' event during server creation logs warning instead of crashing", async () => { const error = Object.assign(new Error('watch failed'), { code: 'EBUSY' }) const logWarn = vi.fn() const logger = createLogger('error') - logger.warn = (...args) => { - logWarn(...args) - resolve() - } + logger.warn = logWarn - const server = await createServerWithPlugin( + await createServerWithPlugin( { name: 'test', + configureServer(server) { + expect(() => server.watcher.emit('error', error)).not.toThrow() + }, }, logger, ) - expect(server.watcher.listenerCount('error')).toBeGreaterThan(0) - expect(() => { - server.watcher.emit('error', error) - }).not.toThrow() - - await promise - expect(logWarn).toHaveBeenCalled() - expect(logWarn.mock.calls[0][0]).toContain('watch failed') + expect(logWarn).toHaveBeenCalledWith( + expect.stringContaining('file watcher error: watch failed'), + ) }) }) From b78e167b9027850db91904e08a0a0d361bbd67ca Mon Sep 17 00:00:00 2001 From: kingmakeruix <300409226+kingmakeruix@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:30:05 +0300 Subject: [PATCH 3/5] fix(server): handle watcher errors during startup --- .../src/node/__tests__/plugins/hooks.spec.ts | 25 +++++++++++-------- packages/vite/src/node/server/index.ts | 11 +++----- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts index cea40eeda6d0a8..e5fb0b26618ebc 100644 --- a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts @@ -7,6 +7,7 @@ import { type Logger, createLogger } from '../../logger' import type { Plugin } from '../../plugin' import { preview } from '../../preview' import { createServer } from '../../server' +import { DevEnvironment } from '../../server/environment' const resolveConfigWithPlugin = ( plugin: Plugin, @@ -446,22 +447,26 @@ describe('watcher add/unlink error handling', () => { expect(logError).toHaveBeenCalledWith(error) }) - test("'error' event during server creation logs warning instead of crashing", async () => { + test("'error' event during environment initialization logs warning instead of crashing", async () => { const error = Object.assign(new Error('watch failed'), { code: 'EBUSY' }) const logWarn = vi.fn() const logger = createLogger('error') logger.warn = logWarn - await createServerWithPlugin( - { - name: 'test', - configureServer(server) { - expect(() => server.watcher.emit('error', error)).not.toThrow() - }, - }, - logger, - ) + const init = DevEnvironment.prototype.init + const initSpy = vi + .spyOn(DevEnvironment.prototype, 'init') + .mockImplementation(function (options) { + const watcher = options?.watcher + if (this.name === 'client' && watcher) { + expect(() => watcher.emit('error', error)).not.toThrow() + } + return init.call(this, options) + }) + onTestFinished(() => initSpy.mockRestore()) + + await createServerWithPlugin({ name: 'test' }, logger) expect(logWarn).toHaveBeenCalledWith( expect.stringContaining('file watcher error: watch failed'), diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 2a0f8561975bb5..c5ecedd3a32abe 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -597,6 +597,10 @@ export async function _createServer( ) as FSWatcher) : createNoopWatcher(resolvedWatchOptions) + watcher.on('error', (error: Error) => { + config.logger.warn(colors.yellow(`file watcher error: ${error.message}`)) + }) + const environments: Record = {} await Promise.all( @@ -967,13 +971,6 @@ export async function _createServer( watcher.on('unlink', (file) => { onFileAddUnlink(file, true).catch((e) => server.config.logger.error(e)) }) - watcher.on('error', (error: Error) => { - // A single unwatchable path (e.g. EBUSY on a briefly locked file) - // must not crash the dev server with an uncaught 'error' event. - server.config.logger.warn( - colors.yellow(`file watcher error: ${error.message}`), - ) - }) if (!middlewareMode && httpServer) { httpServer.once('listening', () => { From 329b2e4f6c7fea874360d1a67af502994cfe64d2 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Wed, 30 Sep 2026 16:10:28 +0900 Subject: [PATCH 4/5] chore: upgrade to error --- packages/vite/src/node/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index b84d5f9fc620ad..9cad2cf24f6145 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -601,7 +601,7 @@ export async function _createServer( : createNoopWatcher(resolvedWatchOptions) watcher.on('error', (error: Error) => { - config.logger.warn(colors.yellow(`file watcher error: ${error.message}`)) + config.logger.error(colors.red(`file watcher error: ${error.message}`)) }) const environments: Record = {} From 93f2bc61191bb67a6ba5b081525f3cfec6574f8d Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Wed, 30 Sep 2026 16:51:52 +0900 Subject: [PATCH 5/5] test: tweak tests --- packages/vite/src/node/__tests__/dev.spec.ts | 36 +++++++++++++++++++ .../src/node/__tests__/plugins/hooks.spec.ts | 27 -------------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/vite/src/node/__tests__/dev.spec.ts b/packages/vite/src/node/__tests__/dev.spec.ts index 66f1b76e6f7f47..9411c004b951b3 100644 --- a/packages/vite/src/node/__tests__/dev.spec.ts +++ b/packages/vite/src/node/__tests__/dev.spec.ts @@ -5,6 +5,7 @@ import { createServer, resolveConfig } from '..' import type { ViteDevServer } from '..' import { promiseWithResolvers } from '../../shared/utils' import { createLogger } from '../logger' +import { DevEnvironment } from '../server/environment' import { normalizePath } from '../utils' describe('resolveBuildEnvironmentOptions in dev', () => { @@ -154,6 +155,41 @@ describe('the dev server', () => { ).rejects.toThrow('buildStart failed') }) + test("logs watcher 'error' events during environment initialization", async () => { + const error = new Error('watch failed') + const logger = createLogger('error') + logger.error = vi.fn() + + class WatcherErrorEnvironment extends DevEnvironment { + override async init( + options?: Parameters[0], + ): Promise { + options?.watcher?.emit('error', error) + await super.init(options) + } + } + + server = await createServer({ + configFile: false, + root: import.meta.dirname, + customLogger: logger, + optimizeDeps: { noDiscovery: true }, + environments: { + ssr: { + dev: { + createEnvironment: (name, config) => + new WatcherErrorEnvironment(name, config, { hot: false }), + }, + }, + }, + server: { middlewareMode: true, ws: false }, + }) + + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('file watcher error: watch failed'), + ) + }) + test('resolves the server URLs before the httpServer listening events are called', async () => { expect.assertions(1) diff --git a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts index e5fb0b26618ebc..d532b2cc57afa2 100644 --- a/packages/vite/src/node/__tests__/plugins/hooks.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/hooks.spec.ts @@ -7,7 +7,6 @@ import { type Logger, createLogger } from '../../logger' import type { Plugin } from '../../plugin' import { preview } from '../../preview' import { createServer } from '../../server' -import { DevEnvironment } from '../../server/environment' const resolveConfigWithPlugin = ( plugin: Plugin, @@ -446,32 +445,6 @@ describe('watcher add/unlink error handling', () => { expect(logError).toHaveBeenCalled() expect(logError).toHaveBeenCalledWith(error) }) - - test("'error' event during environment initialization logs warning instead of crashing", async () => { - const error = Object.assign(new Error('watch failed'), { code: 'EBUSY' }) - - const logWarn = vi.fn() - const logger = createLogger('error') - logger.warn = logWarn - - const init = DevEnvironment.prototype.init - const initSpy = vi - .spyOn(DevEnvironment.prototype, 'init') - .mockImplementation(function (options) { - const watcher = options?.watcher - if (this.name === 'client' && watcher) { - expect(() => watcher.emit('error', error)).not.toThrow() - } - return init.call(this, options) - }) - onTestFinished(() => initSpy.mockRestore()) - - await createServerWithPlugin({ name: 'test' }, logger) - - expect(logWarn).toHaveBeenCalledWith( - expect.stringContaining('file watcher error: watch failed'), - ) - }) }) describe('closeServer hook', () => {