nuxt / nuxt/hints

@nuxt/hints does not work under a non-root app.baseURL (telemetry endpoint, client $fetch, and devtools panel all ignore the base)

Open
#361 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
344
Forks
13
PR merge metrics
No merged PRs in 30d

Description

Summary

When app.baseURL is not /, @nuxt/hints breaks in several independent ways, because it registers its server endpoints and serves its devtools client at bare paths while the browser requests them at base-prefixed paths.

PR #322 (issue #321), shipped in v1.1.0, only base-prefixed the devtools iframe src — it fixed the URL the iframe requests but not the middleware that serves it, and didn't touch the telemetry endpoint at all. So the module is still broken in 1.1.3:

  • the lazy-load / hydration / html-validate telemetry POSTs never reach their handler,
  • those requests fall through to the host app's SSR renderer, producing [Vue Router warn]: No match found … warnings,
  • and the devtools Hints panel renders its own 404 even once it loads, because its base is baked into the shipped client bundle.

A grep over dist/module.mjs + dist/runtime/ confirms the only app.baseURL reference in the whole module/runtime of 1.1.3 is the single line added by #322.

Reproduction

Minimal repro repo (vanilla Nuxt app; only non-default config is adding @nuxt/hints and a non-root app.baseURL):

https://github.com/OliverRC/nuxt-hints-base-url-repro

git clone https://github.com/OliverRC/nuxt-hints-base-url-repro
cd nuxt-hints-base-url-repro
npm install
npm run dev

Load the app (served under the base, e.g. http://localhost:3000/repro/) and open the Nuxt DevTools Hints tab.

Environment

  • @nuxt/hints: 1.1.3 (latest / dist-tags.latest)
  • nuxt: 4.4.8 (latest)
  • Reproducible in nuxt dev

Expected

The telemetry endpoint and the devtools UI resolve under app.baseURL; the Hints panel routes normally; no Vue Router warnings.

Actual

Warnings in the console / server log (requests miss the hints handlers and fall through to the host SSR renderer):

[Vue Router warn]: No match found for location with path "/__nuxt_hints/lazy-load"
[Vue Router warn]: No match found for location with path "/__nuxt_hints/hydration"
[Vue Router warn]: No match found for location with path "/__nuxt-hints"

And the Hints panel renders its own 404 on first load (recovers if you navigate to the panel's home).

Deterministic check (dev server on port 3000, base /repro/):

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/__nuxt_hints/hydration        # 200  bare works
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/repro/__nuxt_hints/hydration  # 404  base-prefixed misses
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/__nuxt-hints                   # 200  bare works
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/repro/__nuxt-hints             # 404  base-prefixed misses

Set app.baseURL back to "/" and every symptom disappears.


Root causes

(1) Telemetry server handler is registered at a bare route

dist/module.mjs (setupDevToolsUI):

addDevServerHandler({ route: "/__nuxt_hints", handler: createHintsRouter().handler })

Registered at bare /__nuxt_hints, but the client sends to ${app.baseURL}/__nuxt_hints/... (see (2)). The base is never applied here.

(2) Client $fetch uses a relative path, which Nuxt prepends with app.baseURL

dist/runtime/lazy-load/plugin.client.js (same pattern for hydration / html-validate):

$fetch(LAZY_LOAD_ROUTE, { method: "POST", body: payload })
// LAZY_LOAD_ROUTE = "/__nuxt_hints/lazy-load"

Nuxt's $fetch prepends app.baseURL, so the real request is /repro/__nuxt_hints/lazy-load — which the bare handler in (1) never matches. The POST falls through to the host SSR renderer → the No match found for "/__nuxt_hints/lazy-load" warning.

(3) Devtools sirv mount is bare, contradicting the #322 iframe fix

dist/module.mjs (setupDevToolsUI):

// iframe src — base-prefixed by #322:
src: joinURL(nuxt.options.app?.baseURL || "/", DEVTOOLS_UI_ROUTE)   // → /repro/__nuxt-hints
// ...but the client is still served at the bare path:
server.middlewares.use(DEVTOOLS_UI_ROUTE, sirv(clientPath, { dev: true, single: true }))   // → /__nuxt-hints

After #322 the iframe requests /repro/__nuxt-hints, which nothing serves → the No match found for "/__nuxt-hints" warning. (Before #322 the iframe pointed at bare /__nuxt-hints, which sirv does serve — so #322 introduced this particular symptom by fixing only half of it.)

(4) The prebuilt devtools client hardcodes its own baseURL, so the panel is unusable under a non-root host base

This is the most fundamental one, and it remains even if (3) is fully fixed (iframe src and sirv mount both base-prefixed). The shipped client bundle bakes its base into the served HTML:

window.__NUXT__.config.app.baseURL = "/__nuxt-hints"

So when the panel is loaded at <host-base>/__nuxt-hints (e.g. /repro/__nuxt-hints), its internal Vue Router strips /__nuxt-hints from /repro/__nuxt-hints, fails to match, and renders the panel's own 404 on first load (navigating to the panel's home recovers it). The devtools client needs to be built/served with — or be agnostic to — the host's app.baseURL; otherwise the Hints tab cannot function under any non-root base.

Additional note (turns the mismatches into visible warnings)

The POST/DELETE handlers (e.g. dist/runtime/lazy-load/handlers.js) call setResponseStatus(...) and then return undefined:

export const postHandler = defineEventHandler(async (event) => {
  // ...validate, store...
  setResponseStatus(event, 201);   // no return value
});

In h3 an undefined return is treated as pass-through middleware, so even once routing is fixed the request continues down the stack to the host's SSR renderer (which is what surfaces the Vue Router warning) instead of terminating. Returning a value (e.g. null) would stop the chain.


Suggested fix

The module already imports joinURL from ufo and has nuxt.options.app.baseURL available:

  • (1) addDevServerHandler({ route: withBase("/__nuxt_hints", base), ... })
  • (2) have the client derive its endpoint from the runtime app.baseURL (or issue a base-agnostic request)
  • (3) server.middlewares.use(withBase(DEVTOOLS_UI_ROUTE, base), sirv(...)) to match the (already base-prefixed) iframe src
  • (4) build/serve the devtools client with the host's app.baseURL, or make its router/asset base relative to where it's mounted
  • have the POST/DELETE handlers return a terminating value instead of undefined

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the reproduction commands and inspect setupDevToolsUI in dist/module.mjs, plus dist/runtime/lazy-load/plugin.client.js and handlers.js. Trace the telemetry routes, sirv mount, iframe URL, and shipped client base configuration under /repro/. Done means telemetry reaches its handlers, the Hints panel loads and routes correctly, and the listed Vue Router warnings no longer occur.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.