Environment
|
|
| Operating System |
Darwin |
| Node Version |
v24.18.0 |
| Nuxt Version |
4.4.6 |
| CLI Version |
3.36.0 |
| Nitro Version |
2.13.4 |
| Package Manager |
npm@11.17.0 |
| Builder |
vite |
| User Config |
compatibilityDate, modules, hints |
| Runtime Modules |
@nuxt/hints@1.1.2 |
| Build Modules |
- |
Reproducible in WebContainer (StackBlitz) and on plain Node.
Describe the bug
The htmlValidate feature registers a Nitro render:response hook that pipes every SSR HTML response through prettier.format(response.body, { plugins: [html], parser: 'html' }) before handing the markup to html-validate. Prettier's HTML parser cannot follow the SVG to HTML namespace transition when an element whose tag name collides with an SVG-2 element (here <picture>) is nested inside <foreignObject>. Prettier throws, the format(...) call is not wrapped in a try/catch, so the render:response hook rejects and the page is served as HTTP 500 with no html-validate report.
This <svg><foreignObject><picture><source><img></picture></foreignObject></svg> shape is exactly what <NuxtImg> expands to when responsive sources are configured, a common pattern for SVG-clipped portraits.
The proof artifact (output of npm run reproduce, which calls prettier.format the same way the plugin does):
Unexpected closing tag ":svg:picture". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags (7:5)
5 | <source type="image/avif" srcset="/img/portrait_275.avif 1x">
6 | <img src="/img/portrait_275.jpg" alt="portrait" width="275" height="275">
> 7 | </picture>
| ^^^^^^^^^^
8 | </foreignObject>
9 | </svg>
The same error surfaces end to end via npm run dev: the page request returns HTTP 500 and the response never contains the <script id="hints-html-validate"> block the plugin inlines on success.
Reproduction
https://stackblitz.com/github/JonathanXDR/repro-nuxt-hints-prettier-foreignobject
Expected behavior
A page containing <svg><foreignObject><picture>...</picture></foreignObject></svg> returns HTTP 200, the html-validate report is stored, and the inline report tag is present in the response body. A formatter failure on one page should not take down the SSR response, since the formatting is purely cosmetic for the inline report. html-validate accepts the original unformatted HTML directly.
Additional context
Root cause is in dist/runtime/html-validate/nitro.plugin.js:
- line 6 to 7 import
format from prettier/standalone and the prettier/parser-html plugin.
- line 35 registers
nitro.hooks.hook('render:response', ...).
- line 37
const formattedBody = await format(response.body, { plugins: [html], parser: 'html' }) runs with no try/catch. This is the call that throws on the namespace transition.
- line 38
await validator.validateString(formattedBody) never runs because line 37 already rejected.
Because the throw is unguarded, the entire render:response hook fails for any page matching this shape, and the only user-side workaround is disabling the whole feature with hints.features.htmlValidate: false (there is no per-page or per-rule opt-out).
Two possible fixes: wrap the format(...) call in a try/catch and fall back to response.body (html-validate accepts unformatted HTML), or validate response.body directly and only run prettier for the human-readable copy stored in the report.
Environment
Darwinv24.18.04.4.63.36.02.13.4npm@11.17.0vitecompatibilityDate,modules,hints@nuxt/hints@1.1.2-Reproducible in WebContainer (StackBlitz) and on plain Node.
Describe the bug
The
htmlValidatefeature registers a Nitrorender:responsehook that pipes every SSR HTML response throughprettier.format(response.body, { plugins: [html], parser: 'html' })before handing the markup to html-validate. Prettier's HTML parser cannot follow the SVG to HTML namespace transition when an element whose tag name collides with an SVG-2 element (here<picture>) is nested inside<foreignObject>. Prettier throws, theformat(...)call is not wrapped in a try/catch, so therender:responsehook rejects and the page is served as HTTP 500 with no html-validate report.This
<svg><foreignObject><picture><source><img></picture></foreignObject></svg>shape is exactly what<NuxtImg>expands to when responsive sources are configured, a common pattern for SVG-clipped portraits.The proof artifact (output of
npm run reproduce, which callsprettier.formatthe same way the plugin does):The same error surfaces end to end via
npm run dev: the page request returns HTTP 500 and the response never contains the<script id="hints-html-validate">block the plugin inlines on success.Reproduction
https://stackblitz.com/github/JonathanXDR/repro-nuxt-hints-prettier-foreignobject
Expected behavior
A page containing
<svg><foreignObject><picture>...</picture></foreignObject></svg>returns HTTP 200, the html-validate report is stored, and the inline report tag is present in the response body. A formatter failure on one page should not take down the SSR response, since the formatting is purely cosmetic for the inline report. html-validate accepts the original unformatted HTML directly.Additional context
Root cause is in
dist/runtime/html-validate/nitro.plugin.js:formatfromprettier/standaloneand theprettier/parser-htmlplugin.nitro.hooks.hook('render:response', ...).const formattedBody = await format(response.body, { plugins: [html], parser: 'html' })runs with no try/catch. This is the call that throws on the namespace transition.await validator.validateString(formattedBody)never runs because line 37 already rejected.Because the throw is unguarded, the entire
render:responsehook fails for any page matching this shape, and the only user-side workaround is disabling the whole feature withhints.features.htmlValidate: false(there is no per-page or per-rule opt-out).Two possible fixes: wrap the
format(...)call in a try/catch and fall back toresponse.body(html-validate accepts unformatted HTML), or validateresponse.bodydirectly and only run prettier for the human-readable copy stored in the report.