plans/011-slide-patch-post-404.md
Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result. If anything in "STOP conditions" occurs, stop and report. When done, update the status row in
plans/README.md.Drift check (run first):
git diff --stat c63cb120..HEAD -- packages/slidev/node/vite/loaders.tsOn a mismatch with the excerpt below, treat it as a STOP condition.
c63cb120, 2026-07-10The dev-server middleware for /__slidev/slides/<n>.json computes
idx = Number.parseInt(no) - 1 and, on POST, immediately dereferences
data.slides[idx].source.content. If n is past the deck length (a stale
editor after a slide was deleted, or a bad request), slide is undefined and
the handler throws inside the async middleware → hung request + unhandled
rejection. The GET branch tolerates a missing slide; POST should too.
packages/slidev/node/vite/loaders.ts:79-138 (inside configureServer):
server.middlewares.use(async (req, res, next) => {
const match = req.url?.match(regexSlideReqPath)
if (!match) return next()
const [, no] = match
const idx = Number.parseInt(no) - 1
if (req.method === 'GET') {
res.write(JSON.stringify(withRenderedNote(data.slides[idx])))
return res.end()
}
else if (req.method === 'POST') {
const body: SlidePatch = await getBodyJson(req)
const slide = data.slides[idx] // ← may be undefined
if (body.content && body.content !== slide.source.content) // ← throws here
hmrSlidesIndexes.add(idx)
// ... more slide.* mutations, then parser.save(...)
}
next()
})
withRenderedNote (loaders.ts:426-432) already handles undefined via
optional chaining, so GET degrades gracefully; only POST is unguarded.
This middleware has no colocated test (the loader is untested today).
| Purpose | Command | Expected |
|---|---|---|
| Install | pnpm install | exit 0 |
| Build | pnpm build | exit 0 |
| Typecheck | pnpm typecheck | exit 0 |
| Lint | pnpm lint | exit 0 |
In scope:
packages/slidev/node/vite/loaders.ts (add a bounds check in the middleware)Out of scope:
getBodyJson or parser.save.fix/slide-patch-bounds.fix(server): 404 on out-of-range slide patch.Right after const idx = Number.parseInt(no) - 1, add a bounds check that
serves a 404 for an out-of-range (or NaN) index, before either branch runs:
const idx = Number.parseInt(no) - 1
const targetSlide = data.slides[idx]
if (!targetSlide) {
res.statusCode = 404
return res.end()
}
Then use targetSlide (or keep the existing data.slides[idx] reads, now known
to be defined) in both the GET and POST branches. Ensure the POST branch's
const slide = data.slides[idx] still resolves to the same object.
Verify: reading the handler, no code path dereferences data.slides[idx]
without the preceding if (!targetSlide) return 404.
Verify: pnpm build && pnpm typecheck && pnpm lint exit 0.
pnpm demo:dev: curl -X POST localhost:<port>/__slidev/slides/9999.json
returns 404 instead of hanging.data.slides[idx] occurs before the bounds checkpnpm build, pnpm typecheck, pnpm lint exit 0loaders.ts modified (git status)plans/README.md status row updatedStop and report if:
regexSlideReqPath allows non-numeric no in a way that makes Number.parseInt
produce a surprising index (re-check vite/common.ts); adjust the guard to
reject NaN explicitly (Number.isNaN(idx)).