From 79a8230cd66f91438a3b532ef257ef9708217e52 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 14 May 2026 16:32:10 +0900 Subject: [PATCH] fix(dev): handle errors when sending messages to vite server --- .../shared/__tests__/forwardConsole.spec.ts | 82 ++++++++++++++++++- packages/vite/src/shared/forwardConsole.ts | 64 +++++++++++---- .../vite/src/shared/moduleRunnerTransport.ts | 11 ++- 3 files changed, 135 insertions(+), 22 deletions(-) diff --git a/packages/vite/src/shared/__tests__/forwardConsole.spec.ts b/packages/vite/src/shared/__tests__/forwardConsole.spec.ts index 44c6b1361f2783..fb3e3880d76f1b 100644 --- a/packages/vite/src/shared/__tests__/forwardConsole.spec.ts +++ b/packages/vite/src/shared/__tests__/forwardConsole.spec.ts @@ -1,5 +1,13 @@ -import { describe, expect, test } from 'vitest' -import { formatConsoleArgs } from '../forwardConsole' +import { setTimeout } from 'node:timers/promises' +import { describe, expect, test, vi } from 'vitest' +import { + formatConsoleArgs, + setupForwardConsoleHandler, +} from '../forwardConsole' +import { + type NormalizedModuleRunnerTransport, + SendBeforeConnectError, +} from '../moduleRunnerTransport' describe('formatConsoleArgs', () => { test('formats placeholders', () => { @@ -59,3 +67,73 @@ describe('formatConsoleArgs', () => { ) }) }) + +describe('setupForwardConsoleHandler', () => { + function createMockConsole() { + return { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + log: vi.fn(), + debug: vi.fn(), + } as unknown as Console + } + + function createMockTransport( + send: (...args: any[]) => Promise, + ): NormalizedModuleRunnerTransport { + return { + connect: () => Promise.resolve(), + disconnect: () => Promise.resolve(), + send, + invoke: () => Promise.resolve({ result: undefined } as any), + } + } + + test('ignore SendBeforeConnectError from transport.send', async () => { + const transport = createMockTransport(() => + Promise.reject(new SendBeforeConnectError('not connected yet')), + ) + const console = createMockConsole() + + setupForwardConsoleHandler( + transport, + { + enabled: true, + unhandledErrors: false, + logLevels: ['log'], + }, + console, + ) + + console.log('hi') + await setTimeout(50) + + expect(console.error).not.toHaveBeenCalled() + }) + + test('log errors from transport.send', async () => { + const transport = createMockTransport(() => + Promise.reject(new Error('other error')), + ) + const console = createMockConsole() + + setupForwardConsoleHandler( + transport, + { + enabled: true, + unhandledErrors: false, + logLevels: ['log'], + }, + console, + ) + + console.log('hi') + await setTimeout(50) + + expect(console.error).toHaveBeenCalledWith( + 'Failed to send error to Vite server:', + new Error('other error'), + ) + }) +}) diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index d675cae366006e..6ee999d9c23c89 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -1,5 +1,8 @@ import type { ForwardConsolePayload } from '#types/customEvent' -import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' +import { + type NormalizedModuleRunnerTransport, + SendBeforeConnectError, +} from './moduleRunnerTransport' export type ForwardConsoleLogLevel = | 'error' @@ -23,13 +26,14 @@ export interface ResolvedForwardConsoleOptions { export function setupForwardConsoleHandler( transport: NormalizedModuleRunnerTransport, options: ResolvedForwardConsoleOptions, + console: Console = globalThis.console, ): void { if (!options.enabled) { return } - function sendError(type: 'error' | 'unhandled-rejection', error: any) { - transport.send({ + async function sendError(type: 'error' | 'unhandled-rejection', error: any) { + await transport.send({ type: 'custom', event: 'vite:forward-console', data: { @@ -43,20 +47,32 @@ export function setupForwardConsoleHandler( }) } - function sendLog(level: ForwardConsoleLogLevel, args: unknown[]) { - transport.send({ - type: 'custom', - event: 'vite:forward-console', - data: { - type: 'log', + async function sendLog(level: ForwardConsoleLogLevel, args: unknown[]) { + try { + await transport.send({ + type: 'custom', + event: 'vite:forward-console', data: { - level, - message: formatConsoleArgs(args), - }, - } satisfies ForwardConsolePayload, - }) + type: 'log', + data: { + level, + message: formatConsoleArgs(args), + }, + } satisfies ForwardConsolePayload, + }) + } catch (err) { + try { + await sendError('unhandled-rejection', err) + } catch (err) { + if (!(err instanceof SendBeforeConnectError)) { + originalConsoleError('Failed to send error to Vite server:', err) + } + } + } } + const originalConsoleError = console.error + for (const level of options.logLevels) { const original = (console as any)[level] if (typeof original !== 'function') { @@ -69,18 +85,30 @@ export function setupForwardConsoleHandler( } if (options.unhandledErrors && typeof window !== 'undefined') { - window.addEventListener('error', (event) => { + window.addEventListener('error', async (event) => { // `ErrorEvent` doesn't necessarily have `ErrorEvent.error`. // Use `ErrorEvent.message` as fallback e.g. for ResizeObserver error. // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent/error // https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors const error = event.error ?? (event.message ? new Error(event.message) : event) - sendError('error', error) + try { + await sendError('error', error) + } catch (err) { + if (!(err instanceof SendBeforeConnectError)) { + originalConsoleError('Failed to send error to Vite server:', err) + } + } }) - window.addEventListener('unhandledrejection', (event) => { - sendError('unhandled-rejection', event.reason) + window.addEventListener('unhandledrejection', async (event) => { + try { + await sendError('unhandled-rejection', event.reason) + } catch (err) { + if (!(err instanceof SendBeforeConnectError)) { + originalConsoleError('Failed to send error to Vite server:', err) + } + } }) } } diff --git a/packages/vite/src/shared/moduleRunnerTransport.ts b/packages/vite/src/shared/moduleRunnerTransport.ts index 2728f81d78e778..bc7b2977a1ac22 100644 --- a/packages/vite/src/shared/moduleRunnerTransport.ts +++ b/packages/vite/src/shared/moduleRunnerTransport.ts @@ -237,7 +237,7 @@ export const normalizeModuleRunnerTransport = ( if (connectingPromise) { await connectingPromise } else { - throw new Error('send was called before connect') + throw new SendBeforeConnectError('send was called before connect') } } await invokeableTransport.send(data) @@ -247,7 +247,7 @@ export const normalizeModuleRunnerTransport = ( if (connectingPromise) { await connectingPromise } else { - throw new Error('invoke was called before connect') + throw new SendBeforeConnectError('invoke was called before connect') } } return invokeableTransport.invoke(name, data) @@ -255,6 +255,13 @@ export const normalizeModuleRunnerTransport = ( } } +export class SendBeforeConnectError extends Error { + constructor(message: string) { + super(message) + this.name = 'SendBeforeConnectError' + } +} + export const createWebSocketModuleRunnerTransport = (options: { // eslint-disable-next-line n/no-unsupported-features/node-builtins createConnection: () => WebSocket