Skip to content

Commit c12a4a7

Browse files
authored
fix(html): allow control character in input stream (vitejs#20483)
1 parent 946831f commit c12a4a7

File tree

7 files changed

+46
-12
lines changed

7 files changed

+46
-12
lines changed

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ pnpm-lock.yaml
77
pnpm-workspace.yaml
88
playground/tsconfig-json-load-error/has-error/tsconfig.json
99
playground/html/invalid.html
10+
playground/html/invalidClick.html
11+
playground/html/invalidEscape.html
1012
playground/html/valid.html
1113
playground/external/public/[email protected]
1214
playground/ssr-html/public/[email protected]

packages/vite/src/node/plugins/html.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import type {
99
} from 'rollup'
1010
import MagicString from 'magic-string'
1111
import colors from 'picocolors'
12-
import type { DefaultTreeAdapterMap, ParserError, Token } from 'parse5'
12+
import type {
13+
DefaultTreeAdapterMap,
14+
ErrorCodes,
15+
ParserError,
16+
Token,
17+
} from 'parse5'
1318
import { stripLiteral } from 'strip-literal'
1419
import escapeHtml from 'escape-html'
1520
import type { MinimalPluginContextWithoutEnvironment, Plugin } from '../plugin'
@@ -34,6 +39,7 @@ import { resolveEnvPrefix } from '../env'
3439
import { cleanUrl } from '../../shared/utils'
3540
import { perEnvironmentState } from '../environment'
3641
import { getNodeAssetAttributes } from '../assetSource'
42+
import type { Logger } from '../logger'
3743
import {
3844
assetUrlRE,
3945
getPublicAssetFilename,
@@ -184,21 +190,29 @@ function traverseNodes(
184190
}
185191
}
186192

193+
type ParseWarnings = Partial<Record<ErrorCodes, string>>
194+
187195
export async function traverseHtml(
188196
html: string,
189197
filePath: string,
198+
warn: Logger['warn'],
190199
visitor: (node: DefaultTreeAdapterMap['node']) => void,
191200
): Promise<void> {
192201
// lazy load compiler
193202
const { parse } = await import('parse5')
203+
const warnings: ParseWarnings = {}
194204
const ast = parse(html, {
195205
scriptingEnabled: false, // parse inside <noscript>
196206
sourceCodeLocationInfo: true,
197207
onParseError: (e: ParserError) => {
198-
handleParseError(e, html, filePath)
208+
handleParseError(e, html, filePath, warnings)
199209
},
200210
})
201211
traverseNodes(ast, visitor)
212+
213+
for (const message of Object.values(warnings)) {
214+
warn(colors.yellow(`\n${message}`))
215+
}
202216
}
203217

204218
export function getScriptInfo(node: DefaultTreeAdapterMap['element']): {
@@ -297,6 +311,7 @@ function handleParseError(
297311
parserError: ParserError,
298312
html: string,
299313
filePath: string,
314+
warnings: ParseWarnings,
300315
) {
301316
switch (parserError.code) {
302317
case 'missing-doctype':
@@ -318,11 +333,10 @@ function handleParseError(
318333
return
319334
}
320335
const parseError = formatParseError(parserError, filePath, html)
321-
throw new Error(
336+
warnings[parseError.code] ??=
322337
`Unable to parse HTML; ${parseError.message}\n` +
323-
` at ${parseError.loc.file}:${parseError.loc.line}:${parseError.loc.column}\n` +
324-
`${parseError.frame}`,
325-
)
338+
` at ${parseError.loc.file}:${parseError.loc.line}:${parseError.loc.column}\n` +
339+
`${parseError.frame.length > 300 ? '[this code frame is omitted as the content was too long] ' : parseError.frame}`
326340
}
327341

328342
/**
@@ -442,7 +456,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
442456
}
443457

444458
const setModuleSideEffectPromises: Promise<void>[] = []
445-
await traverseHtml(html, id, (node) => {
459+
await traverseHtml(html, id, config.logger.warn, (node) => {
446460
if (!nodeIsElement(node)) {
447461
return
448462
}
@@ -1238,7 +1252,7 @@ export function injectNonceAttributeTagHook(
12381252

12391253
const s = new MagicString(html)
12401254

1241-
await traverseHtml(html, filename, (node) => {
1255+
await traverseHtml(html, filename, config.logger.warn, (node) => {
12421256
if (!nodeIsElement(node)) {
12431257
return
12441258
}

packages/vite/src/node/server/middlewares/indexHtml.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
269269
preTransformRequest(server!, modulePath, decodedBase)
270270
}
271271

272-
await traverseHtml(html, filename, (node) => {
272+
await traverseHtml(html, filename, config.logger.warn, (node) => {
273273
if (!nodeIsElement(node)) {
274274
return
275275
}

playground/html/__tests__/html.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,11 @@ describe.runIf(isServe)('invalid', () => {
284284
const message = await errorOverlay.$$eval('.message-body', (m) => {
285285
return m[0].innerHTML
286286
})
287-
expect(message).toMatch(/^Unable to parse HTML/)
287+
expect(message).toContain('Unable to parse HTML')
288288
})
289289

290290
test('should close overlay when clicked away', async () => {
291-
await page.goto(viteTestUrl + '/invalid.html')
291+
await page.goto(viteTestUrl + '/invalidClick.html')
292292
const errorOverlay = await page.waitForSelector('vite-error-overlay')
293293
expect(errorOverlay).toBeTruthy()
294294

@@ -298,7 +298,7 @@ describe.runIf(isServe)('invalid', () => {
298298
})
299299

300300
test('should close overlay when escape key is pressed', async () => {
301-
await page.goto(viteTestUrl + '/invalid.html')
301+
await page.goto(viteTestUrl + '/invalidEscape.html')
302302
const errorOverlay = await page.waitForSelector('vite-error-overlay')
303303
expect(errorOverlay).toBeTruthy()
304304

playground/html/invalidClick.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div Bad CLICK HTML</div>

playground/html/invalidEscape.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div Bad ESCAPE HTML</div>

playground/vitestSetup.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ export function setViteUrl(url: string): void {
7676
viteTestUrl = url
7777
}
7878

79+
function throwHtmlParseError() {
80+
return {
81+
name: 'vite-plugin-throw-html-parse-error',
82+
configResolved(config: ResolvedConfig) {
83+
const warn = config.logger.warn
84+
config.logger.warn = (msg, opts) => {
85+
// convert HTML parse warnings to make it easier to test
86+
if (msg.includes('Unable to parse HTML;')) {
87+
throw new Error(msg)
88+
}
89+
warn.call(config.logger, msg, opts)
90+
}
91+
},
92+
}
93+
}
7994
// #endregion
8095

8196
beforeAll(async (s) => {
@@ -224,6 +239,7 @@ async function loadConfig(configEnv: ConfigEnv) {
224239
emptyOutDir: false,
225240
},
226241
customLogger: createInMemoryLogger(serverLogs),
242+
plugins: [throwHtmlParseError()],
227243
}
228244
return mergeConfig(options, config || {})
229245
}

0 commit comments

Comments
 (0)