Conversation
commit: |
📝 WalkthroughWalkthroughThis pull request refactors the hints system from Server-Sent Events (SSE)-based communication to Remote Procedure Call (RPC)-based messaging. The client-side SSE listener plugins are removed and replaced with RPC handler registration and direct HTTP endpoint calls. Server-side changes consolidate feature-specific handling (hydration, lazy-load, HTML validate) from distributed Nitro plugins into centralized handler modules that maintain in-memory state and expose HTTP GET/POST/DELETE endpoints. A new RPC bridge wires Nitro hooks to client-side RPC broadcasts. The devtools integration is updated to register server RPC functions and use the new HTTP router for backend communication. The architectural shift eliminates the intermediate SSE layer, replacing event streaming with direct HTTP calls and RPC invocations. Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/runtime/types.d.ts (1)
1-67:⚠️ Potential issue | 🔴 CriticalMissing
NitroRuntimeHookstype augmentation causes pipeline failure.The PR removed the
nitropackmodule augmentation that typed the custom Nitro hooks (hints:hydration:mismatch,hints:lazy-load:report,hints:html-validate:report, etc.). This causes TypeScript errors insrc/runtime/core/server/rpc-bridge.tswhere these hooks are used.Re-add the
NitroRuntimeHooksaugmentation or move it to an appropriate location (e.g., alongsiderpc-bridge.ts):declare module 'nitropack' { interface NitroRuntimeHooks { 'hints:hydration:mismatch': (payload: HydrationMismatchPayload) => void 'hints:hydration:cleared': (payload: { id: string[] }) => void 'hints:lazy-load:report': (data: ComponentLazyLoadData) => void 'hints:lazy-load:cleared': (payload: { id: string }) => void 'hints:html-validate:report': (report: HtmlValidateReport) => void 'hints:html-validate:deleted': (id: string) => void } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/types.d.ts` around lines 1 - 67, Re-add the missing TypeScript module augmentation for 'nitropack' to restore the NitroRuntimeHooks interface (so rpc-bridge.ts compiles); declare the hooks 'hints:hydration:mismatch', 'hints:hydration:cleared', 'hints:lazy-load:report', 'hints:lazy-load:cleared', 'hints:html-validate:report', and 'hints:html-validate:deleted' on NitroRuntimeHooks and reference the existing payload types used elsewhere (HydrationMismatchPayload, ComponentLazyLoadData, HtmlValidateReport); place this declaration in src/runtime/types.d.ts (or next to rpc-bridge.ts) and ensure the referenced types are imported/available in that module augmentation.src/module.ts (1)
86-90:⚠️ Potential issue | 🟠 MajorHTML-validate reports will silently fail when devtools is disabled.
The
htmlValidatefeature is enabled independently viaisFeatureEnabled(options, 'htmlValidate')at lines 87-90, but the HTTP endpoint for hints (/__nuxt_hints) is registered only insidesetupDevToolsUI(), which is called conditionally whenoptions.devtoolsis true. The Nitro plugin at line 89 will attempt to POST reports to a non-existent endpoint, resulting in silent 404 failures.Consider either registering the hints router unconditionally when any feature needs it, or making the Nitro plugin check if the endpoint exists before posting.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/module.ts` around lines 86 - 90, The html-validate Nitro plugin posts to the /__nuxt_hints endpoint but that hints router is only registered inside setupDevToolsUI() when options.devtools is true, causing 404s when htmlValidate is enabled without devtools; fix by ensuring the hints router is registered whenever any feature needs it (e.g., extract the hints router registration from setupDevToolsUI() into a new function and call it when isFeatureEnabled(options, 'htmlValidate') is true before addServerPlugin(resolver.resolve('./runtime/html-validate/nitro.plugin')), or alternatively modify the Nitro plugin (the file resolved by resolver.resolve('./runtime/html-validate/nitro.plugin')) to detect the /__nuxt_hints endpoint (or handle 404s) before attempting to POST; update references to setupDevToolsUI(), isFeatureEnabled, addServerPlugin and the nitro plugin accordingly.
🧹 Nitpick comments (2)
src/runtime/html-validate/api-handlers.ts (1)
31-41: Minimal validation on POST body.The validation only checks that
body.idis a string, butHtmlValidateReportlikely requires additional fields (path,html,results). Malformed payloads will be stored and could cause issues downstream.Consider validating the complete shape or using a schema validator like valibot (already used in
lazy-load/handlers.ts).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/html-validate/api-handlers.ts` around lines 31 - 41, The postHandler currently only verifies body.id; update validation to ensure the full HtmlValidateReport shape is present before pushing to htmlValidateReports and calling onReport: validate that body.path and body.html are strings and that body.results is an array (or use the existing valibot schema pattern from lazy-load/handlers.ts to validate the structure returned by readBody<HtmlValidateReport>(event)); if validation fails, throw createError({ statusCode: 400, ... }) and do not mutate htmlValidateReports or call onReport; keep setResponseStatus(event, 201) only after successful validation.src/runtime/html-validate/nitro.plugin.ts (1)
57-62: Silent error swallowing hides failures.The fetch call silently discards all errors with
.catch(() => {}). If the/__nuxt_hints/html-validateendpoint is unavailable (e.g., devtools disabled) or the request fails, there's no indication in logs.Consider at least logging errors in development:
Proposed improvement
const origin = getRequestURL(event).origin globalThis.fetch(`${origin}/__nuxt_hints/html-validate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), - }).catch(() => {}) + }).catch((err) => { + console.debug('[hints:html-validate] Failed to report validation:', err.message) + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/html-validate/nitro.plugin.ts` around lines 57 - 62, The fetch to the '/__nuxt_hints/html-validate' endpoint currently swallows all errors via .catch(() => {}); update the error handling for the globalThis.fetch call (used with getRequestURL(event).origin) to at minimum log the caught error (e.g., console.error or the project logger) in non-production/dev builds so failed requests are visible during development; ensure the catch provides context like "html-validate POST failed" and includes the error object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@client/app/app.vue`:
- Around line 22-54: The live RPC handlers (registered in
onDevtoolsClientConnected: onHydrationMismatch, onHydrationCleared,
onLazyLoadReport, onLazyLoadCleared, onHtmlValidateReport,
onHtmlValidateDeleted) can reintroduce items when the initial snapshot from the
three get*() calls arrives; fix by either buffering live updates until initial
snapshots are applied or tracking tombstones seen during initial sync: create
per-collection tombstone Sets (e.g., hydrationTombstones, lazyLoadTombstones,
htmlValidateTombstones) and/or an initialSyncDone flag, have each live handler
check the tombstone set and skip adding items whose id is in the tombstone (or
if initialSyncDone is false, enqueue the update), and when each get*().then(...)
merges the snapshot, remove tombstoned ids from the result and flush buffered
updates; reference the onDevtoolsClientConnected registration and the handler
names above to locate where to add tombstone logic or buffering.
In `@src/runtime/core/server/rpc-bridge.ts`:
- Around line 1-6: Add missing type declarations by creating a .d.ts file (e.g.,
src/runtime/types.d.ts) that (1) declares the global var __nuxtHintsRpcBroadcast
typed as HintsClientFunctions | undefined to satisfy uses of
globalThis.__nuxtHintsRpcBroadcast (reference HintsClientFunctions from
src/runtime/core/rpc-types) and (2) re-augment the NitroRuntimeHooks interface
to include the missing hook names removed from the project so the Nitro hook
usages compile; ensure the file uses declare global { var
__nuxtHintsRpcBroadcast: HintsClientFunctions | undefined } and the appropriate
module augmentation for NitroRuntimeHooks.
- Around line 13-14: The hook "hints:hydration:cleared" handler is passing
payload.id (singular) to getRpcBroadcast()?.onHydrationCleared which expects
ids: string[]; update the hook payload and call so the handler provides an
array: change the hook signature/type to use ids: string[] (instead of id) and
call getRpcBroadcast()?.onHydrationCleared(payload.ids); alternatively, if
payload must remain singular, wrap it into an array when calling
onHydrationCleared (e.g., [payload.id]); update the nitroApp.hooks.hook handler
and any related type/interface that defines the hook payload to reflect ids:
string[].
- Around line 8-31: The exported default function registering Nitro hooks (hook
names: 'hints:hydration:mismatch', 'hints:hydration:cleared',
'hints:lazy-load:report', 'hints:lazy-load:cleared',
'hints:html-validate:report', 'hints:html-validate:deleted') is dead because
getRpcBroadcast() is never populated (globalThis.__nuxtHintsRpcBroadcast) and
the codebase uses direct callbacks via setHydrationNotify, setLazyLoadNotify,
setHtmlValidateNotify that call rpc.broadcast; either remove this bridge
entirely, or rewire it by (a) ensuring devtools.ts assigns
globalThis.__nuxtHintsRpcBroadcast so getRpcBroadcast() returns the broadcast
implementation, and (b) updating the handlers that currently call
setHydrationNotify/setLazyLoadNotify/setHtmlValidateNotify to also trigger the
corresponding Nitro hooks (e.g.,
nitroApp.hooks.callHook('hints:hydration:mismatch', ...) or emit equivalent) so
the bridge receives events—pick one approach and apply it consistently (delete
unused default export and its hook registrations if removing, or add the global
assignment in devtools.ts and the hook emissions in the handler functions if
reconnecting).
In `@src/runtime/html-validate/api-handlers.ts`:
- Around line 21-27: clearHtmlValidateReport currently calls onDeleted?.(id)
unconditionally, causing notifications for IDs that weren't found; change the
function so it only invokes onDeleted when a matching report was actually
removed — i.e., check the result of htmlValidateReports.findIndex (index !== -1)
and, inside that branch after splice, call onDeleted?.(id) (do not call it when
no report was found).
In `@src/runtime/hydration/handlers.ts`:
- Around line 43-47: When the 20-item ring buffer drops the oldest entry in
hydrationMismatches, also notify clients so they can remove the evicted mismatch
from their local state (clients currently only remove items via
onHydrationCleared and will otherwise drift from getHydrationMismatches()).
Change the eviction logic in the block that currently does
hydrationMismatches.push(payload) / hydrationMismatches.shift() to capture the
shifted item (const evicted = hydrationMismatches.shift()) and call the client
notification callback (e.g., onHydrationCleared?.(evicted) or the appropriate
signature your system uses) immediately after eviction; keep onMismatch(payload)
as-is for the new mismatch notification.
- Around line 61-69: The assertPayload function currently treats null as an
object and dereferences fields, causing a TypeError; change the initial guard to
explicitly reject null (e.g., check body === null or !body) before accessing
properties so null bodies fail the assertion and trigger a 400; update the guard
in assertPayload (used by postHandler) to mirror the defensive pattern used in
deleteHandler by ensuring body is truthy and an object before checking
htmlPreHydration, htmlPostHydration, componentName, and fileLocation.
In `@src/runtime/lazy-load/handlers.ts`:
- Around line 23-29: POST /lazy-load must be idempotent by id: in the
postHandler, do not always push into lazyLoadData — locate an existing item in
lazyLoadData by item.id and either update/replace it or skip adding if
identical; use findIndex(item => item.id === id) and set lazyLoadData[index] =
newItem when found, otherwise push. Also make deletion/clearing remove all
duplicates: update deleteHandler and clearLazyLoadHint to remove every entry
with the given id (e.g., lazyLoadData = lazyLoadData.filter(item => item.id !==
id)) so a duplicate doesn't reappear on the next GET. Ensure you reference and
modify the lazyLoadData array and the functions postHandler, deleteHandler, and
clearLazyLoadHint accordingly.
---
Outside diff comments:
In `@src/module.ts`:
- Around line 86-90: The html-validate Nitro plugin posts to the /__nuxt_hints
endpoint but that hints router is only registered inside setupDevToolsUI() when
options.devtools is true, causing 404s when htmlValidate is enabled without
devtools; fix by ensuring the hints router is registered whenever any feature
needs it (e.g., extract the hints router registration from setupDevToolsUI()
into a new function and call it when isFeatureEnabled(options, 'htmlValidate')
is true before
addServerPlugin(resolver.resolve('./runtime/html-validate/nitro.plugin')), or
alternatively modify the Nitro plugin (the file resolved by
resolver.resolve('./runtime/html-validate/nitro.plugin')) to detect the
/__nuxt_hints endpoint (or handle 404s) before attempting to POST; update
references to setupDevToolsUI(), isFeatureEnabled, addServerPlugin and the nitro
plugin accordingly.
In `@src/runtime/types.d.ts`:
- Around line 1-67: Re-add the missing TypeScript module augmentation for
'nitropack' to restore the NitroRuntimeHooks interface (so rpc-bridge.ts
compiles); declare the hooks 'hints:hydration:mismatch',
'hints:hydration:cleared', 'hints:lazy-load:report', 'hints:lazy-load:cleared',
'hints:html-validate:report', and 'hints:html-validate:deleted' on
NitroRuntimeHooks and reference the existing payload types used elsewhere
(HydrationMismatchPayload, ComponentLazyLoadData, HtmlValidateReport); place
this declaration in src/runtime/types.d.ts (or next to rpc-bridge.ts) and ensure
the referenced types are imported/available in that module augmentation.
---
Nitpick comments:
In `@src/runtime/html-validate/api-handlers.ts`:
- Around line 31-41: The postHandler currently only verifies body.id; update
validation to ensure the full HtmlValidateReport shape is present before pushing
to htmlValidateReports and calling onReport: validate that body.path and
body.html are strings and that body.results is an array (or use the existing
valibot schema pattern from lazy-load/handlers.ts to validate the structure
returned by readBody<HtmlValidateReport>(event)); if validation fails, throw
createError({ statusCode: 400, ... }) and do not mutate htmlValidateReports or
call onReport; keep setResponseStatus(event, 201) only after successful
validation.
In `@src/runtime/html-validate/nitro.plugin.ts`:
- Around line 57-62: The fetch to the '/__nuxt_hints/html-validate' endpoint
currently swallows all errors via .catch(() => {}); update the error handling
for the globalThis.fetch call (used with getRequestURL(event).origin) to at
minimum log the caught error (e.g., console.error or the project logger) in
non-production/dev builds so failed requests are visible during development;
ensure the catch provides context like "html-validate POST failed" and includes
the error object.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dbaa79ce-c8c4-4d7b-9bf0-5214df7c94f7
📒 Files selected for processing (27)
client/app/app.vueclient/app/plugins/0.sse.tsclient/app/plugins/html-validate.tsclient/app/plugins/hydration.tsclient/app/plugins/lazy-load.tsclient/app/utils/routes.tssrc/devtools-handlers.tssrc/devtools.tssrc/module.tssrc/runtime/core/rpc-types.tssrc/runtime/core/server/rpc-bridge.tssrc/runtime/core/server/sse.tssrc/runtime/core/server/types.tssrc/runtime/html-validate/api-handlers.tssrc/runtime/html-validate/handlers/delete.tssrc/runtime/html-validate/handlers/get.tssrc/runtime/html-validate/handlers/nitro-handlers.plugin.tssrc/runtime/html-validate/nitro.plugin.tssrc/runtime/html-validate/storage.tssrc/runtime/hydration/handlers.tssrc/runtime/hydration/nitro.plugin.tssrc/runtime/hydration/types.tssrc/runtime/hydration/utils.tssrc/runtime/lazy-load/handlers.tssrc/runtime/lazy-load/nitro.plugin.tssrc/runtime/types.d.tstest/unit/core/sse.test.ts
💤 Files with no reviewable changes (14)
- src/runtime/html-validate/storage.ts
- client/app/utils/routes.ts
- src/runtime/core/server/sse.ts
- src/runtime/html-validate/handlers/get.ts
- client/app/plugins/0.sse.ts
- src/runtime/html-validate/handlers/nitro-handlers.plugin.ts
- src/runtime/html-validate/handlers/delete.ts
- test/unit/core/sse.test.ts
- client/app/plugins/lazy-load.ts
- client/app/plugins/hydration.ts
- src/runtime/hydration/types.ts
- client/app/plugins/html-validate.ts
- src/runtime/hydration/nitro.plugin.ts
- src/runtime/lazy-load/nitro.plugin.ts
| onDevtoolsClientConnected((client) => { | ||
| const rpc = client.devtools.extendClientRpc<HintsServerFunctions, HintsClientFunctions>(RPC_NAMESPACE, { | ||
| onHydrationMismatch(mismatch: HydrationMismatchPayload) { | ||
| if (!hydrationMismatches.value.some(existing => existing.id === mismatch.id)) { | ||
| hydrationMismatches.value.push(mismatch) | ||
| } | ||
| }, | ||
| onHydrationCleared(ids: string[]) { | ||
| hydrationMismatches.value = hydrationMismatches.value.filter(m => !ids.includes(m.id)) | ||
| }, | ||
| onLazyLoadReport(data: ComponentLazyLoadData) { | ||
| try { | ||
| const validated = parse(ComponentLazyLoadDataSchema, data) | ||
| if (!lazyLoadHints.value.some(existing => existing.id === validated.id)) { | ||
| lazyLoadHints.value.push(validated) | ||
| } | ||
| } | ||
| catch { | ||
| console.warn('[hints] Ignoring malformed lazy-load report', data) | ||
| } | ||
| }, | ||
| onLazyLoadCleared(id: string) { | ||
| lazyLoadHints.value = lazyLoadHints.value.filter(entry => entry.id !== id) | ||
| }, | ||
| onHtmlValidateReport(report: HtmlValidateReport) { | ||
| if (!htmlValidateReports.value.some(existing => existing.id === report.id)) { | ||
| htmlValidateReports.value = [...htmlValidateReports.value, report] | ||
| } | ||
| }, | ||
| onHtmlValidateDeleted(id: string) { | ||
| htmlValidateReports.value = htmlValidateReports.value.filter(report => report.id !== id) | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Avoid reintroducing cleared items from the initial RPC snapshot.
The live handlers are active before the three get*() calls settle. If a clear/delete arrives in that window, the later then(...) merge sees the id as absent locally and appends it back from the stale response. That can resurrect already-cleared hydration, lazy-load, or html-validate entries until the next refresh. Track tombstones during the initial sync, or buffer live updates until each initial snapshot has been applied.
Also applies to: 57-84
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@client/app/app.vue` around lines 22 - 54, The live RPC handlers (registered
in onDevtoolsClientConnected: onHydrationMismatch, onHydrationCleared,
onLazyLoadReport, onLazyLoadCleared, onHtmlValidateReport,
onHtmlValidateDeleted) can reintroduce items when the initial snapshot from the
three get*() calls arrives; fix by either buffering live updates until initial
snapshots are applied or tracking tombstones seen during initial sync: create
per-collection tombstone Sets (e.g., hydrationTombstones, lazyLoadTombstones,
htmlValidateTombstones) and/or an initialSyncDone flag, have each live handler
check the tombstone set and skip adding items whose id is in the tombstone (or
if initialSyncDone is false, enqueue the update), and when each get*().then(...)
merges the snapshot, remove tombstoned ids from the result and flush buffered
updates; reference the onDevtoolsClientConnected registration and the handler
names above to locate where to add tombstone logic or buffering.
| import type { NitroApp } from 'nitropack/types' | ||
| import type { HintsClientFunctions } from '../rpc-types' | ||
|
|
||
| function getRpcBroadcast(): HintsClientFunctions | undefined { | ||
| return globalThis.__nuxtHintsRpcBroadcast | ||
| } |
There was a problem hiding this comment.
TypeScript errors: missing type declarations.
The pipeline is failing due to multiple TypeScript errors:
globalThis.__nuxtHintsRpcBroadcastneeds a type declaration- The Nitro hook names are not recognized because
NitroRuntimeHooksaugmentation was removed
Add type declarations to fix:
Proposed fix for globalThis typing
Add to a .d.ts file (e.g., src/runtime/types.d.ts or a new file):
import type { HintsClientFunctions } from './core/rpc-types'
declare global {
// eslint-disable-next-line no-var
var __nuxtHintsRpcBroadcast: HintsClientFunctions | undefined
}🧰 Tools
🪛 GitHub Actions: ci
[error] 5-5: TypeScript (tsc) error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
🪛 GitHub Check: ci
[failure] 5-5:
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/core/server/rpc-bridge.ts` around lines 1 - 6, Add missing type
declarations by creating a .d.ts file (e.g., src/runtime/types.d.ts) that (1)
declares the global var __nuxtHintsRpcBroadcast typed as HintsClientFunctions |
undefined to satisfy uses of globalThis.__nuxtHintsRpcBroadcast (reference
HintsClientFunctions from src/runtime/core/rpc-types) and (2) re-augment the
NitroRuntimeHooks interface to include the missing hook names removed from the
project so the Nitro hook usages compile; ensure the file uses declare global {
var __nuxtHintsRpcBroadcast: HintsClientFunctions | undefined } and the
appropriate module augmentation for NitroRuntimeHooks.
| export default function (nitroApp: NitroApp) { | ||
| nitroApp.hooks.hook('hints:hydration:mismatch', (mismatch) => { | ||
| getRpcBroadcast()?.onHydrationMismatch(mismatch) | ||
| }) | ||
|
|
||
| nitroApp.hooks.hook('hints:hydration:cleared', (payload) => { | ||
| getRpcBroadcast()?.onHydrationCleared(payload.id) | ||
| }) | ||
|
|
||
| nitroApp.hooks.hook('hints:lazy-load:report', (data) => { | ||
| getRpcBroadcast()?.onLazyLoadReport(data) | ||
| }) | ||
|
|
||
| nitroApp.hooks.hook('hints:lazy-load:cleared', (payload) => { | ||
| getRpcBroadcast()?.onLazyLoadCleared(payload.id) | ||
| }) | ||
|
|
||
| nitroApp.hooks.hook('hints:html-validate:report', (report) => { | ||
| getRpcBroadcast()?.onHtmlValidateReport(report) | ||
| }) | ||
|
|
||
| nitroApp.hooks.hook('hints:html-validate:deleted', (id) => { | ||
| getRpcBroadcast()?.onHtmlValidateDeleted(id) | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if anything sets globalThis.__nuxtHintsRpcBroadcast
rg -n '__nuxtHintsRpcBroadcast' --type ts
# Check if any code fires the hints:* Nitro hooks
rg -n "hooks\.(call|emit|hook).*hints:" --type tsRepository: nuxt/hints
Length of output: 765
🏁 Script executed:
# Find the handler files mentioned
fd -e ts hydration/handlers lazy-load/handlers html-validate/api-handlers devtools-handlers devtools
# Search for the callback functions mentioned
rg -n "setHydrationNotify|setLazyLoadNotify|setHtmlValidateNotify" --type ts
# Search for setBroadcast function
rg -n "setBroadcast" --type ts
# Check if hooks are actually called/emitted
rg -n "hooks\.(call|emit)" --type tsRepository: nuxt/hints
Length of output: 1107
🏁 Script executed:
# Read devtools.ts to see setBroadcast context
sed -n '65,75p' src/devtools.ts
# Read devtools-handlers.ts to see setBroadcast and callbacks
sed -n '35,50p' src/devtools-handlers.ts
# Read the handler files to see what setHydrationNotify does
sed -n '1,30p' src/runtime/hydration/handlers.ts
sed -n '1,30p' src/runtime/lazy-load/handlers.ts
sed -n '1,25p' src/runtime/html-validate/api-handlers.tsRepository: nuxt/hints
Length of output: 3215
🏁 Script executed:
# Read full rpc-bridge.ts
cat -n src/runtime/core/server/rpc-bridge.ts
# Check if hooks are ever called/emitted (not just hooked)
rg -n "hooks\.call|hooks\.emit" --type ts
# Check if globalThis.__nuxtHintsRpcBroadcast is ever assigned
rg -n "__nuxtHintsRpcBroadcast\s*=" --type tsRepository: nuxt/hints
Length of output: 1267
Bridge integration is dead code — remove or reconnect.
This bridge listens for Nitro hooks (hints:hydration:mismatch, hints:lazy-load:report, etc.), but the codebase does not fire these hooks. The actual notification flow uses direct callbacks: handlers call setHydrationNotify/setLazyLoadNotify/setHtmlValidateNotify with callbacks that invoke rpc.broadcast methods, bypassing this bridge entirely. Additionally, globalThis.__nuxtHintsRpcBroadcast is never assigned in the codebase, so the getRpcBroadcast() function always returns undefined.
Either remove this dead code, or refactor to fire the Nitro hooks from the handlers and set globalThis.__nuxtHintsRpcBroadcast in devtools.ts.
🧰 Tools
🪛 GitHub Check: ci
[failure] 25-25:
Argument of type '"hints:html-validate:report"' is not assignable to parameter of type 'HookKeys'.
[failure] 21-21:
Parameter 'payload' implicitly has an 'any' type.
[failure] 21-21:
Argument of type '"hints:lazy-load:cleared"' is not assignable to parameter of type 'HookKeys'.
[failure] 17-17:
Parameter 'data' implicitly has an 'any' type.
[failure] 17-17:
Argument of type '"hints:lazy-load:report"' is not assignable to parameter of type 'HookKeys'.
[failure] 13-13:
Parameter 'payload' implicitly has an 'any' type.
[failure] 13-13:
Argument of type '"hints:hydration:cleared"' is not assignable to parameter of type 'HookKeys'.
[failure] 9-9:
Parameter 'mismatch' implicitly has an 'any' type.
[failure] 9-9:
Argument of type '"hints:hydration:mismatch"' is not assignable to parameter of type 'HookKeys'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/core/server/rpc-bridge.ts` around lines 8 - 31, The exported
default function registering Nitro hooks (hook names:
'hints:hydration:mismatch', 'hints:hydration:cleared', 'hints:lazy-load:report',
'hints:lazy-load:cleared', 'hints:html-validate:report',
'hints:html-validate:deleted') is dead because getRpcBroadcast() is never
populated (globalThis.__nuxtHintsRpcBroadcast) and the codebase uses direct
callbacks via setHydrationNotify, setLazyLoadNotify, setHtmlValidateNotify that
call rpc.broadcast; either remove this bridge entirely, or rewire it by (a)
ensuring devtools.ts assigns globalThis.__nuxtHintsRpcBroadcast so
getRpcBroadcast() returns the broadcast implementation, and (b) updating the
handlers that currently call
setHydrationNotify/setLazyLoadNotify/setHtmlValidateNotify to also trigger the
corresponding Nitro hooks (e.g.,
nitroApp.hooks.callHook('hints:hydration:mismatch', ...) or emit equivalent) so
the bridge receives events—pick one approach and apply it consistently (delete
unused default export and its hook registrations if removing, or add the global
assignment in devtools.ts and the hook emissions in the handler functions if
reconnecting).
| nitroApp.hooks.hook('hints:hydration:cleared', (payload) => { | ||
| getRpcBroadcast()?.onHydrationCleared(payload.id) |
There was a problem hiding this comment.
Type mismatch: payload.id vs ids array.
The hints:hydration:cleared hook handler passes payload.id (singular), but HintsClientFunctions.onHydrationCleared expects ids: string[] (array) based on the interface in rpc-types.ts.
Proposed fix
nitroApp.hooks.hook('hints:hydration:cleared', (payload) => {
- getRpcBroadcast()?.onHydrationCleared(payload.id)
+ getRpcBroadcast()?.onHydrationCleared(payload.ids)
})Also update the hook type to use ids: string[] instead of id.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| nitroApp.hooks.hook('hints:hydration:cleared', (payload) => { | |
| getRpcBroadcast()?.onHydrationCleared(payload.id) | |
| nitroApp.hooks.hook('hints:hydration:cleared', (payload) => { | |
| getRpcBroadcast()?.onHydrationCleared(payload.ids) | |
| }) |
🧰 Tools
🪛 GitHub Check: ci
[failure] 13-13:
Parameter 'payload' implicitly has an 'any' type.
[failure] 13-13:
Argument of type '"hints:hydration:cleared"' is not assignable to parameter of type 'HookKeys'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/core/server/rpc-bridge.ts` around lines 13 - 14, The hook
"hints:hydration:cleared" handler is passing payload.id (singular) to
getRpcBroadcast()?.onHydrationCleared which expects ids: string[]; update the
hook payload and call so the handler provides an array: change the hook
signature/type to use ids: string[] (instead of id) and call
getRpcBroadcast()?.onHydrationCleared(payload.ids); alternatively, if payload
must remain singular, wrap it into an array when calling onHydrationCleared
(e.g., [payload.id]); update the nitroApp.hooks.hook handler and any related
type/interface that defines the hook payload to reflect ids: string[].
| export function clearHtmlValidateReport(id: string) { | ||
| const index = htmlValidateReports.findIndex(r => r.id === id) | ||
| if (index !== -1) { | ||
| htmlValidateReports.splice(index, 1) | ||
| } | ||
| onDeleted?.(id) | ||
| } |
There was a problem hiding this comment.
onDeleted callback invoked even when report doesn't exist.
The clearHtmlValidateReport function calls onDeleted?.(id) unconditionally, even when no matching report was found. This can cause spurious RPC broadcasts to clients for non-existent IDs.
Proposed fix
export function clearHtmlValidateReport(id: string) {
const index = htmlValidateReports.findIndex(r => r.id === id)
if (index !== -1) {
htmlValidateReports.splice(index, 1)
+ onDeleted?.(id)
}
- onDeleted?.(id)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function clearHtmlValidateReport(id: string) { | |
| const index = htmlValidateReports.findIndex(r => r.id === id) | |
| if (index !== -1) { | |
| htmlValidateReports.splice(index, 1) | |
| } | |
| onDeleted?.(id) | |
| } | |
| export function clearHtmlValidateReport(id: string) { | |
| const index = htmlValidateReports.findIndex(r => r.id === id) | |
| if (index !== -1) { | |
| htmlValidateReports.splice(index, 1) | |
| onDeleted?.(id) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/html-validate/api-handlers.ts` around lines 21 - 27,
clearHtmlValidateReport currently calls onDeleted?.(id) unconditionally, causing
notifications for IDs that weren't found; change the function so it only invokes
onDeleted when a matching report was actually removed — i.e., check the result
of htmlValidateReports.findIndex (index !== -1) and, inside that branch after
splice, call onDeleted?.(id) (do not call it when no report was found).
| hydrationMismatches.push(payload) | ||
| if (hydrationMismatches.length > 20) { | ||
| hydrationMismatches.shift() | ||
| } | ||
| onMismatch?.(payload) |
There was a problem hiding this comment.
Notify clients when the 20-item buffer evicts an old mismatch.
Once this buffer rolls over, the oldest entry is dropped server-side but only onMismatch is emitted. Connected clients only remove hydration items through onHydrationCleared, so they keep evicted mismatches forever and drift away from getHydrationMismatches().
Proposed fix
hydrationMismatches.push(payload)
if (hydrationMismatches.length > 20) {
- hydrationMismatches.shift()
+ const removed = hydrationMismatches.shift()
+ if (removed) {
+ onCleared?.([removed.id])
+ }
}
onMismatch?.(payload)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| hydrationMismatches.push(payload) | |
| if (hydrationMismatches.length > 20) { | |
| hydrationMismatches.shift() | |
| } | |
| onMismatch?.(payload) | |
| hydrationMismatches.push(payload) | |
| if (hydrationMismatches.length > 20) { | |
| const removed = hydrationMismatches.shift() | |
| if (removed) { | |
| onCleared?.([removed.id]) | |
| } | |
| } | |
| onMismatch?.(payload) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/hydration/handlers.ts` around lines 43 - 47, When the 20-item
ring buffer drops the oldest entry in hydrationMismatches, also notify clients
so they can remove the evicted mismatch from their local state (clients
currently only remove items via onHydrationCleared and will otherwise drift from
getHydrationMismatches()). Change the eviction logic in the block that currently
does hydrationMismatches.push(payload) / hydrationMismatches.shift() to capture
the shifted item (const evicted = hydrationMismatches.shift()) and call the
client notification callback (e.g., onHydrationCleared?.(evicted) or the
appropriate signature your system uses) immediately after eviction; keep
onMismatch(payload) as-is for the new mismatch notification.
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| function assertPayload(body: any): asserts body is Omit<HydrationMismatchPayload, 'id'> { | ||
| if ( | ||
| typeof body !== 'object' | ||
| || (body.htmlPreHydration !== undefined && typeof body.htmlPreHydration !== 'string') | ||
| || (body.htmlPostHydration !== undefined && typeof body.htmlPostHydration !== 'string') | ||
| || typeof body.componentName !== 'string' | ||
| || typeof body.fileLocation !== 'string' | ||
| ) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
node - <<'NODE'
const body = null
console.log('typeof null =', typeof body)
try {
console.log(body.htmlPreHydration)
}
catch (error) {
console.log(error.name)
}
NODERepository: nuxt/hints
Length of output: 84
🏁 Script executed:
cat -n src/runtime/hydration/handlers.ts | head -100Repository: nuxt/hints
Length of output: 2997
🏁 Script executed:
rg "readBody" src/runtime/hydration/handlers.ts -B 2 -A 2Repository: nuxt/hints
Length of output: 649
🏁 Script executed:
# Check if there are any tests that might clarify readBody behavior
fd -e test -e spec | head -20Repository: nuxt/hints
Length of output: 36
🌐 Web query:
h3 readBody function returns null empty body
💡 Result:
In the Rust h3 crate (hyperium/h3, an HTTP/3 implementation), there is no function named readBody. The query likely refers to reading the request body on the server side using RequestStream::recv_data. recv_data returns Result<Option, StreamError>. It returns Ok(None) (null/empty in the sense of no more data) when the client has finished sending the body (stream fin received), not necessarily when the body is empty. This is normal behavior for requests with no body (e.g., GET) or after reading all body chunks. To read the full body: let mut stream = ...; // from resolver.resolve_request let mut body = Vec::new; while let Some(chunk) = stream.recv_data.await? { body.extend_from_slice(&chunk); } If you're seeing None immediately, the client sent no body. Check the request method and Content-Length header. For GET requests, expect empty body. This matches the JavaScript h3js behavior where readBody returns empty/undefined for no-body requests (sources 1,3). Official docs: https://docs.rs/h3/latest/h3/server/struct.RequestStream.html#method.recv_data
Citations:
- 1: https://docs.rs/h3/latest/h3/server/struct.RequestStream.html
- 2: https://docs.rs/h3/latest/h3/server/index.html
- 3: https://docs.rs/h3/latest/src/h3/server/stream.rs.html
- 4: https://docs.rs/h3/latest/h3/client/struct.RequestStream.html
- 5: https://docs.rs/h3/latest/h3/quic/trait.RecvStream.html
🌐 Web query:
h3 JavaScript server framework readBody function documentation unjs
💡 Result:
h3 is a minimal HTTP framework from UnJS (unjs.io). The readBody(event, options?: { strict?: boolean }) utility reads the request body from an H3Event and parses it safely, handling application/json (via JSON.parse) and application/x-www-form-urlencoded (via URLSearchParams). It uses destr for safe parsing in some versions. Usage example: import { defineEventHandler, readBody } from 'h3'; export default defineEventHandler(async (event) => { const body = await readBody(event); // body is any (Promise) return { received: body }; }); Key notes: - Returns Promise by default (TypeScript issue discussed in GitHub #386). - For JSON content-type, validates and throws if invalid JSON (strict mode). - Warns about CSRF risk for non-POST methods or unchecked content-type. - Use readValidatedBody(event, validateFn) for schema validation (e.g., with Zod). - Alternatives: readRawBody for raw string/Buffer, readFormData, readMultipartFormData. Official docs: https://h3.unjs.io/utils/request and https://h3.dev/utils/request
Citations:
- 1: https://h3.dev/utils/request
- 2: https://h3.unjs.io/utils/request
- 3: https://www.h3.dev/utils/request
- 4: https://unpkg.com/browse/h3@1.8.2/README.md
🌐 Web query:
h3 JavaScript framework unhandled error exception HTTP status code 500
💡 Result:
In the H3 JavaScript framework, unhandled errors during the request lifecycle automatically result in an HTTP 500 Internal Server Error status code response. H3 captures any error thrown without using HTTPError and processes it as an unhandled error, sending a 500 response. For security, sensitive fields like stack traces and data are hidden in production JSON responses. To properly handle errors and set custom status codes: - Throw HTTPError: throw new HTTPError({ status: 500, message: "Custom error" }) or throw HTTPError.status(500, "Internal Server Error"). - Use createError (older/compatible): throw createError({ statusCode: 500, message: "Error" }). - Global error handling: Configure onError hook in H3 app: new H3({ onError: (error, event) => { console.error(error); } }) or middleware app.use(onError((error, event) => { ... })). Unhandled errors (e.g., plain throw new Error) default to 500. Examples from GitHub issues show common unhandled 500s in Nuxt/H3 setups due to proxy or session bugs, logged as "[nuxt] [request error] [unhandled] [500]".
Citations:
- 1: https://h3.dev/guide/basics/error
- 2: https://h3.dev/guide/basics/response
- 3: https://h3.dev/guide/api/h3
- 4: https://v1.h3.dev/guide/event-handler
- 5: h3-session: ERROR [nuxt] [request error] [unhandled] [500] Cannot set properties of undefined (setting 'content-type') h3js/h3#770
- 6: Global hooks are not called when an error is thrown h3js/h3#695
- 7: [nuxt] [request error] [unhandled] [500] using
proxyRequestorsendProxyh3js/h3#376 - 8: https://h3.unjs.io/guide/event-handler
Handle null request bodies before dereferencing fields.
In JavaScript, typeof null === 'object'. A null body gets past the first guard and then body.htmlPreHydration throws a TypeError on line 65, which causes an unhandled exception resulting in a 500 error instead of the intended 400.
The deleteHandler already demonstrates the correct pattern with an explicit !body check on line 54 before accessing properties. The postHandler needs the same defensive check.
Proposed fix
// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
function assertPayload(body: any): asserts body is Omit<HydrationMismatchPayload, 'id'> {
if (
+ body == null
+ || typeof body !== 'object'
+ || Array.isArray(body)
|| (body.htmlPreHydration !== undefined && typeof body.htmlPreHydration !== 'string')
|| (body.htmlPostHydration !== undefined && typeof body.htmlPostHydration !== 'string')
|| typeof body.componentName !== 'string'
|| typeof body.fileLocation !== 'string'
) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/hydration/handlers.ts` around lines 61 - 69, The assertPayload
function currently treats null as an object and dereferences fields, causing a
TypeError; change the initial guard to explicitly reject null (e.g., check body
=== null or !body) before accessing properties so null bodies fail the assertion
and trigger a 400; update the guard in assertPayload (used by postHandler) to
mirror the defensive pattern used in deleteHandler by ensuring body is truthy
and an object before checking htmlPreHydration, htmlPostHydration,
componentName, and fileLocation.
| export function clearLazyLoadHint(id: string) { | ||
| const index = lazyLoadData.findIndex(item => item.id === id) | ||
| if (index !== -1) { | ||
| lazyLoadData.splice(index, 1) | ||
| } | ||
| onCleared?.(id) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Make POST /lazy-load idempotent by id.
The rest of this flow treats id as the primary key, but postHandler always appends. If the same hint is reported twice, lazyLoadData keeps duplicates and clearLazyLoadHint/deleteHandler only remove one row, so the hint can come back on the next GET.
Proposed fix
export const postHandler = defineEventHandler(async (event) => {
const body = await readBody(event)
let parsed: ComponentLazyLoadData
try {
parsed = parse(ComponentLazyLoadDataSchema, body)
@@
}
throw error
}
- lazyLoadData.push(parsed)
+ const index = lazyLoadData.findIndex(item => item.id === parsed.id)
+ if (index === -1) {
+ lazyLoadData.push(parsed)
+ setResponseStatus(event, 201)
+ }
+ else {
+ lazyLoadData[index] = parsed
+ setResponseStatus(event, 200)
+ }
onReport?.(parsed)
- setResponseStatus(event, 201)
})Also applies to: 33-49
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/lazy-load/handlers.ts` around lines 23 - 29, POST /lazy-load must
be idempotent by id: in the postHandler, do not always push into lazyLoadData —
locate an existing item in lazyLoadData by item.id and either update/replace it
or skip adding if identical; use findIndex(item => item.id === id) and set
lazyLoadData[index] = newItem when found, otherwise push. Also make
deletion/clearing remove all duplicates: update deleteHandler and
clearLazyLoadHint to remove every entry with the given id (e.g., lazyLoadData =
lazyLoadData.filter(item => item.id !== id)) so a duplicate doesn't reappear on
the next GET. Ensure you reference and modify the lazyLoadData array and the
functions postHandler, deleteHandler, and clearLazyLoadHint accordingly.
This PRs move SSE to devtools RPC so we stick close to devtool's API. (and also to test devtools v4 with vite-devtools)