Skip to content

refactor: move SSE to devtools RPC#318

Open
huang-julien wants to merge 2 commits intomainfrom
refactor/devtoolsrpc
Open

refactor: move SSE to devtools RPC#318
huang-julien wants to merge 2 commits intomainfrom
refactor/devtoolsrpc

Conversation

@huang-julien
Copy link
Copy Markdown
Member

This PRs move SSE to devtools RPC so we stick close to devtool's API. (and also to test devtools v4 with vite-devtools)

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/hints@318

commit: d79edc5

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

This 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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: move SSE to devtools RPC' directly and clearly describes the primary architectural change—migrating from SSE-based communication to devtools RPC-based communication.
Description check ✅ Passed The description explains the rationale (aligning with devtools API and testing devtools v4 with vite-devtools) and relates to the changeset, though it is brief and lacks implementation detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/devtoolsrpc

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

Missing NitroRuntimeHooks type augmentation causes pipeline failure.

The PR removed the nitropack module augmentation that typed the custom Nitro hooks (hints:hydration:mismatch, hints:lazy-load:report, hints:html-validate:report, etc.). This causes TypeScript errors in src/runtime/core/server/rpc-bridge.ts where these hooks are used.

Re-add the NitroRuntimeHooks augmentation or move it to an appropriate location (e.g., alongside rpc-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 | 🟠 Major

HTML-validate reports will silently fail when devtools is disabled.

The htmlValidate feature is enabled independently via isFeatureEnabled(options, 'htmlValidate') at lines 87-90, but the HTTP endpoint for hints (/__nuxt_hints) is registered only inside setupDevToolsUI(), which is called conditionally when options.devtools is 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.id is a string, but HtmlValidateReport likely 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-validate endpoint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 86b47d2 and d79edc5.

📒 Files selected for processing (27)
  • client/app/app.vue
  • client/app/plugins/0.sse.ts
  • client/app/plugins/html-validate.ts
  • client/app/plugins/hydration.ts
  • client/app/plugins/lazy-load.ts
  • client/app/utils/routes.ts
  • src/devtools-handlers.ts
  • src/devtools.ts
  • src/module.ts
  • src/runtime/core/rpc-types.ts
  • src/runtime/core/server/rpc-bridge.ts
  • src/runtime/core/server/sse.ts
  • src/runtime/core/server/types.ts
  • src/runtime/html-validate/api-handlers.ts
  • src/runtime/html-validate/handlers/delete.ts
  • src/runtime/html-validate/handlers/get.ts
  • src/runtime/html-validate/handlers/nitro-handlers.plugin.ts
  • src/runtime/html-validate/nitro.plugin.ts
  • src/runtime/html-validate/storage.ts
  • src/runtime/hydration/handlers.ts
  • src/runtime/hydration/nitro.plugin.ts
  • src/runtime/hydration/types.ts
  • src/runtime/hydration/utils.ts
  • src/runtime/lazy-load/handlers.ts
  • src/runtime/lazy-load/nitro.plugin.ts
  • src/runtime/types.d.ts
  • test/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

Comment on lines +22 to +54
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)
},
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1 to +6
import type { NitroApp } from 'nitropack/types'
import type { HintsClientFunctions } from '../rpc-types'

function getRpcBroadcast(): HintsClientFunctions | undefined {
return globalThis.__nuxtHintsRpcBroadcast
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

TypeScript errors: missing type declarations.

The pipeline is failing due to multiple TypeScript errors:

  1. globalThis.__nuxtHintsRpcBroadcast needs a type declaration
  2. The Nitro hook names are not recognized because NitroRuntimeHooks augmentation 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.

Comment on lines +8 to +31
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)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 ts

Repository: 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 ts

Repository: 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.ts

Repository: 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 ts

Repository: 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).

Comment on lines +13 to +14
nitroApp.hooks.hook('hints:hydration:cleared', (payload) => {
getRpcBroadcast()?.onHydrationCleared(payload.id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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[].

Comment on lines +21 to +27
export function clearHtmlValidateReport(id: string) {
const index = htmlValidateReports.findIndex(r => r.id === id)
if (index !== -1) {
htmlValidateReports.splice(index, 1)
}
onDeleted?.(id)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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).

Comment on lines +43 to +47
hydrationMismatches.push(payload)
if (hydrationMismatches.length > 20) {
hydrationMismatches.shift()
}
onMismatch?.(payload)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +61 to +69
// 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'
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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)
}
NODE

Repository: nuxt/hints

Length of output: 84


🏁 Script executed:

cat -n src/runtime/hydration/handlers.ts | head -100

Repository: nuxt/hints

Length of output: 2997


🏁 Script executed:

rg "readBody" src/runtime/hydration/handlers.ts -B 2 -A 2

Repository: 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 -20

Repository: 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:


🌐 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:


🌐 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:


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.

Comment on lines +23 to +29
export function clearLazyLoadHint(id: string) {
const index = lazyLoadData.findIndex(item => item.id === id)
if (index !== -1) {
lazyLoadData.splice(index, 1)
}
onCleared?.(id)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant