Skip to content

Commit cb5b24f

Browse files
committed
fix(dev): keep the bundler's own probes out of the failed request badge
1 parent 7825f5f commit cb5b24f

2 files changed

Lines changed: 97 additions & 6 deletions

File tree

packages/nuxt-cli/src/dev/tui/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -368,17 +368,18 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {})
368368
return
369369
}
370370
requests.push(batch.map(request => ({ time: Date.now(), ...request })))
371-
const failed = batch.filter(request => request.status >= 500)
371+
// The bundler's own probes 503 while a restart is in flight
372+
const app = batch.filter(request => !request.internal)
373+
const failed = app.filter(request => request.status >= 500)
372374
// Nuxt answers a failed render with its error page rather than logging it,
373375
// so the response status is the only signal that something is wrong.
374-
const failing = (batch.at(-1)?.status ?? 0) >= 500
376+
const failing = (app.at(-1)?.status ?? 0) >= 500
377+
const recovered = app.length > 0 && !failing && state.status === 'error' && !state.errors
375378
update({
376379
active: true,
377380
failures: (state.failures ?? 0) + failed.length,
378-
status: failing
379-
? 'error'
380-
: state.status === 'error' && !state.errors ? 'ready' : state.status,
381-
note: failing ? 'a request failed · press n to trace it' : state.note,
381+
status: failing ? 'error' : recovered ? 'ready' : state.status,
382+
note: failing ? 'a request failed · press n to trace it' : recovered ? undefined : state.note,
382383
})
383384
repaintTicker()
384385
clearTimeout(activityTimer)

packages/nuxt-cli/test/unit/dev-tui.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,3 +2081,93 @@ describe('dev ui fallback', () => {
20812081
expect(setupDevUI(context as never, { enabled: false }).interactive).toBe(false)
20822082
})
20832083
})
2084+
2085+
/** Long enough for the panel's trailing repaint to land. */
2086+
const TICKER_SETTLE_MS = 400
2087+
2088+
describe('request failures on the panel', () => {
2089+
const context = {
2090+
listener: { url: 'http://localhost:3000/', getURLs: () => [], showURLs: () => {} },
2091+
close: async () => {},
2092+
onReady: () => {},
2093+
}
2094+
2095+
async function withPanel(run: (ui: ReturnType<typeof setupDevUI>, settle: () => Promise<string>) => Promise<void>): Promise<void> {
2096+
const chunks: string[] = []
2097+
const saved = (['isTTY', 'columns', 'rows'] as const).map(key => [key, Object.getOwnPropertyDescriptor(process.stdout, key)] as const)
2098+
const stdin = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY')
2099+
const setRawMode = Object.getOwnPropertyDescriptor(process.stdin, 'setRawMode')
2100+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
2101+
Object.defineProperty(process.stdout, 'columns', { value: 100, configurable: true })
2102+
Object.defineProperty(process.stdout, 'rows', { value: 30, configurable: true })
2103+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true })
2104+
Object.defineProperty(process.stdin, 'setRawMode', { value: () => process.stdin, configurable: true })
2105+
const write = vi.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
2106+
chunks.push(String(chunk))
2107+
return true
2108+
})
2109+
const session = beginDevUI({ ci: false, test: false, version: '4.5.2' })!
2110+
const ui = setupDevUI(context as never, { ci: false, test: false, version: '4.5.2' })
2111+
try {
2112+
await run(ui, async () => {
2113+
// The panel repaints on a trailing timer, so nothing is on screen yet.
2114+
await new Promise(resolve => setTimeout(resolve, TICKER_SETTLE_MS))
2115+
return strip(chunks.join(''))
2116+
})
2117+
}
2118+
finally {
2119+
session.teardown()
2120+
write.mockRestore()
2121+
for (const [key, descriptor] of saved) {
2122+
if (descriptor) {
2123+
Object.defineProperty(process.stdout, key, descriptor)
2124+
}
2125+
}
2126+
if (stdin) {
2127+
Object.defineProperty(process.stdin, 'isTTY', stdin)
2128+
}
2129+
if (setRawMode) {
2130+
Object.defineProperty(process.stdin, 'setRawMode', setRawMode)
2131+
}
2132+
else {
2133+
Reflect.deleteProperty(process.stdin, 'setRawMode')
2134+
}
2135+
}
2136+
}
2137+
2138+
it('should not report the bundler\'s own failed probes as failed requests', async () => {
2139+
await withPanel(async (ui, settle) => {
2140+
ui.setStatus('building')
2141+
ui.pushRequests([{ method: 'GET', url: '/__skip_vite', status: 503, duration: 1, internal: true }])
2142+
const frames = await settle()
2143+
2144+
expect(frames).not.toContain('failed request')
2145+
expect(frames).not.toContain('a request failed')
2146+
})
2147+
})
2148+
2149+
it('should report a failed app request', async () => {
2150+
await withPanel(async (ui, settle) => {
2151+
ui.setStatus('ready')
2152+
ui.pushRequests([{ method: 'GET', url: '/', status: 500, duration: 1 }])
2153+
const frames = await settle()
2154+
2155+
expect(frames).toContain('1 failed request')
2156+
expect(frames).toContain('a request failed')
2157+
})
2158+
})
2159+
2160+
it('should not let an internal request clear a failure the app reported', async () => {
2161+
await withPanel(async (ui, settle) => {
2162+
ui.setStatus('ready')
2163+
ui.pushRequests([{ method: 'GET', url: '/', status: 500, duration: 1 }])
2164+
await settle()
2165+
ui.pushRequests([{ method: 'GET', url: '/__skip_vite', status: 200, duration: 1, internal: true }])
2166+
const frames = await settle()
2167+
2168+
const last = frames.slice(frames.lastIndexOf('Nuxt 4.5.2'))
2169+
expect(last).toContain('ERROR')
2170+
expect(last).toContain('a request failed')
2171+
})
2172+
})
2173+
})

0 commit comments

Comments
 (0)