返回 slidev
011-slide-patch-post-404.md
根目录 / plans / 011-slide-patch-post-404.md
1 # Plan 011: Return 404 on out-of-range slide-patch requests
2
3 > **Executor instructions**: Follow this plan step by step. Run every
4 > verification command and confirm the expected result. If anything in "STOP
5 > conditions" occurs, stop and report. When done, update the status row in
6 > `plans/README.md`.
7 >
8 > **Drift check (run first)**: `git diff --stat c63cb120..HEAD -- packages/slidev/node/vite/loaders.ts`
9 > On a mismatch with the excerpt below, treat it as a STOP condition.
10
11 ## Status
12
13 - **Priority**: P3
14 - **Effort**: S
15 - **Risk**: LOW
16 - **Depends on**: none
17 - **Category**: bug
18 - **Planned at**: commit `c63cb120`, 2026-07-10
19
20 ## Why this matters
21
22 The dev-server middleware for `/__slidev/slides/<n>.json` computes
23 `idx = Number.parseInt(no) - 1` and, on `POST`, immediately dereferences
24 `data.slides[idx].source.content`. If `n` is past the deck length (a stale
25 editor after a slide was deleted, or a bad request), `slide` is `undefined` and
26 the handler throws inside the async middleware → hung request + unhandled
27 rejection. The `GET` branch tolerates a missing slide; `POST` should too.
28
29 ## Current state
30
31 `packages/slidev/node/vite/loaders.ts:79-138` (inside `configureServer`):
32 ```ts
33 server.middlewares.use(async (req, res, next) => {
34 const match = req.url?.match(regexSlideReqPath)
35 if (!match) return next()
36 const [, no] = match
37 const idx = Number.parseInt(no) - 1
38 if (req.method === 'GET') {
39 res.write(JSON.stringify(withRenderedNote(data.slides[idx])))
40 return res.end()
41 }
42 else if (req.method === 'POST') {
43 const body: SlidePatch = await getBodyJson(req)
44 const slide = data.slides[idx] // ← may be undefined
45 if (body.content && body.content !== slide.source.content) // ← throws here
46 hmrSlidesIndexes.add(idx)
47 // ... more slide.* mutations, then parser.save(...)
48 }
49 next()
50 })
51 ```
52 `withRenderedNote` (`loaders.ts:426-432`) already handles `undefined` via
53 optional chaining, so `GET` degrades gracefully; only `POST` is unguarded.
54
55 This middleware has no colocated test (the loader is untested today).
56
57 ## Commands you will need
58
59 | Purpose | Command | Expected |
60 |---------|---------|----------|
61 | Install | `pnpm install` | exit 0 |
62 | Build | `pnpm build` | exit 0 |
63 | Typecheck | `pnpm typecheck` | exit 0 |
64 | Lint | `pnpm lint` | exit 0 |
65
66 ## Scope
67
68 **In scope**:
69 - `packages/slidev/node/vite/loaders.ts` (add a bounds check in the middleware)
70
71 **Out of scope**:
72 - The unauthenticated-endpoint concern (plans 015/018).
73 - The HMR/no-op-utils issues (plan 012).
74 - Any change to `getBodyJson` or `parser.save`.
75
76 ## Git workflow
77
78 - Branch: `fix/slide-patch-bounds`.
79 - Conventional commit: `fix(server): 404 on out-of-range slide patch`.
80 - Do NOT push/PR unless instructed.
81
82 ## Steps
83
84 ### Step 1: Guard both branches on a missing slide
85
86 Right after `const idx = Number.parseInt(no) - 1`, add a bounds check that
87 serves a 404 for an out-of-range (or NaN) index, before either branch runs:
88 ```ts
89 const idx = Number.parseInt(no) - 1
90 const targetSlide = data.slides[idx]
91 if (!targetSlide) {
92 res.statusCode = 404
93 return res.end()
94 }
95 ```
96 Then use `targetSlide` (or keep the existing `data.slides[idx]` reads, now known
97 to be defined) in both the `GET` and `POST` branches. Ensure the `POST` branch's
98 `const slide = data.slides[idx]` still resolves to the same object.
99
100 **Verify**: reading the handler, no code path dereferences `data.slides[idx]`
101 without the preceding `if (!targetSlide) return 404`.
102
103 ### Step 2: Build / typecheck / lint
104
105 **Verify**: `pnpm build && pnpm typecheck && pnpm lint` exit 0.
106
107 ## Test plan
108
109 - The loader middleware has no unit harness today, so no automated test is added
110 in this plan (adding one is plan 022's scope). The fix is a defensive
111 bounds-check verified by code review + typecheck. Optional manual check with a
112 running `pnpm demo:dev`: `curl -X POST localhost:<port>/__slidev/slides/9999.json`
113 returns `404` instead of hanging.
114
115 ## Done criteria
116
117 - [ ] An out-of-range/NaN slide index returns HTTP 404 for both GET and POST
118 - [ ] No dereference of `data.slides[idx]` occurs before the bounds check
119 - [ ] `pnpm build`, `pnpm typecheck`, `pnpm lint` exit 0
120 - [ ] Only `loaders.ts` modified (`git status`)
121 - [ ] `plans/README.md` status row updated
122
123 ## STOP conditions
124
125 Stop and report if:
126
127 - `regexSlideReqPath` allows non-numeric `no` in a way that makes `Number.parseInt`
128 produce a surprising index (re-check `vite/common.ts`); adjust the guard to
129 reject NaN explicitly (`Number.isNaN(idx)`).
130
131 ## Maintenance notes
132
133 - When plan 022 adds loader tests, add a case for the out-of-range POST → 404.
134 - Reviewer: confirm the 404 doesn't break the editor's normal save flow (valid
135 indices are unaffected).
136
136 lines MARKDOWN