`defineHandler(middleware, handler)` breaks page loaders and actions: 500 on GET, empty 204 after an action, validator types dropped
維護者通常 1 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 55/100
- Issue 類型
- 缺陷
- 描述清晰度
- 描述清楚
- 活躍度
- 活躍
- 技術堆疊
- typescript
研究方向
Read packages/void/src/runtime/handler.ts around lines 288–303 and trace how its composed handler returns responses; then follow runLoader() and extractProps() in packages/void/src/pages/protocol.ts. Reproduce the loader and action cases from the issue, including validation failure, and check that page responses render with fresh props or field errors and that validator types reach void/routes.
由索引模型根據 Issue 內容生成。
描述
Description
defineHandler(middleware..., handler) is documented for per-route middleware, and the loader and action guides say they use "the same defineHandler API as server routes". In routes/ it works. In a page's .server.ts it breaks both loaders and actions, even with a middleware that only calls next():
- A loader with middleware makes the page fail with a 500.
- An action with middleware returns an empty
204instead of the re-rendered page, and a validation failure returns the raw400JSON instead of the page with field errors. - The action's validator types are lost, so
useForm('/db?create', …)accepts any body.
Steps to reproduce
// pages/db.server.ts
import { defineHandler, defineMiddleware } from 'void';
import * as z from 'zod';
const passThrough = defineMiddleware(async (_c, next) => {
await next();
});
// Case A: loader with middleware
export const loader = defineHandler(passThrough, async () => {
return { notes: await getNotes() };
});
// Case B: action with middleware (test with a plain `defineHandler(async () => …)` loader, since case A fails first)
export const actions = {
create: defineHandler(
passThrough,
defineHandler.withValidator({
body: z.object({ title: z.string().min(1) }),
})(async (_c, { body }) => {
await createNote(body);
}),
),
};
# Case A
curl -i http://localhost:5173/db
curl -i -H 'X-VoidPages: true' http://localhost:5173/db
# Case B
curl -i -X POST 'http://localhost:5173/db?create' -H 'Origin: http://localhost:5173' \
-H 'Content-Type: application/json' -H 'X-VoidPages: true' -d '{"title":"a"}'
curl -i -X POST 'http://localhost:5173/db?create' -H 'Origin: http://localhost:5173' \
-H 'Content-Type: application/json' -H 'X-VoidPages: true' -d '{"title":""}'
Actual behavior
| Request | Without middleware | With passThrough |
|---|---|---|
GET /db (HTML or X-VoidPages) |
200, the page |
500 TypeError: Body has already been used. It can only be used once. |
Valid POST /db?create |
200, the page with fresh props |
204 No Content, empty body (the note is created) |
Invalid POST /db?create |
200, the page with errors: { title: '…' } |
400 with the raw {"error":"Validation failed","issues":[…]} |
Types: ResolveActionBody<'/db?create'> falls back to Record<string, unknown>. { title: 1 } type-checks and form.data.title is unknown.
Expected behavior
The middleware form behaves like the plain form: the loader renders the page, an action re-renders it with fresh props, a validation failure renders it with errors, and the validator's body type reaches void/routes.
Cause
packages/void/src/runtime/handler.ts (main @ 6ba3dbcc3a, lines 288–303): the composed handler stores the final result in c.res and returns c.res:
} else {
c.res = convertReturnValue(await handler(c));
}
// …
return c.res;
Hono's c.res setter sets c.finalized = true, and Hono's compose only adopts a Response returned by a later handler while finalized === false. Page code in packages/void/src/pages/protocol.ts then reads that Response and returns a new page response, which Hono drops and replaces with c.res:
- Loader:
runLoader()→extractProps()readsc.res's JSON body, so Hono sends a body that was already read → 500. - Action that returns nothing:
c.resis the204fromconvertReturnValue(undefined), and that is what's sent. - Validation failure:
c.resiswithValidator's400. Since #517 it is read viaclone(), so it's sent as is. On 1.1.12 this was a 500 (This ReadableStream is disturbed).
In routes/ the returned Response is the final answer anyway, so nothing is lost there.
Types: the middleware overloads return TypedHandler<{}, …>, so the __validators of a withValidator handler passed last are dropped. #490 fixed __output for loaders, not the validators.
Possible fix
- Runtime: have the composed handler return the handler's raw result (or a middleware's short-circuit Response) instead of assigning it to
c.res, so page code gets the same value it gets without middleware. Middleware that editsc.resafternext()(e.g. Hono'setag()) should keep working inroutes/, so theconvertReturnValuestep may need to move to the route adapter. - Types: an overload that keeps the last handler's type, e.g.
defineHandler<V, R>(mw: MiddlewareFn, handler: TypedHandler<V, R>): TypedHandler<V, R>.
Workaround we use for now: a small withMiddleware(...middleware, handler) that runs the chain itself, returns the handler's result untouched, and is typed as the handler (H extends TypedHandler).
Related
- #489 / #490: the loader output type with middleware (fixed in 1.2.0). #489 notes that the runtime already unwraps a loader's return value as props, but on 1.2.1 the page itself fails (case A).
- #517: the
clone()change for 400 responses from actions.
Environment
- void 1.2.1, @void/react 1.2.1 (the action cases also reproduce on 1.1.12)
- hono 4.13.13, zod 4
- vite-plus 1.3.0, Node 22.23.3, macOS 26.6
- 主要語言
- TypeScript
- 星號
- 402
- 分支
- 32
- 平均合併
- 8 小時 32 分鐘
- 30 天內合併 PR
- 111
環境準備
這個專案沒有提供開發容器、Dockerfile 或貢獻指南,環境需要你自己搭建:先看它的 README,通用步驟見我們的新手貢獻指南。
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
voidzero-dev/void 的其他 Issue
-
難度 2/5 1-3 小時 新手友好度 76/100
voidzero-dev/void#565 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 72/100
voidzero-dev/void#529 ·
維護者通常 1 天內回覆
-
bug
難度 1/5 1-3 小時 新手友好度 78/100
voidzero-dev/void#428 ·
維護者通常 1 天內回覆
-
難度 4/5 3-5 天 新手友好度 53/100
voidzero-dev/void#567 ·
維護者通常 1 天內回覆
-
Navigation commit failure leaves the loading state active and causes inconsistent internal state未關閉
難度 4/5 3-5 天 新手友好度 48/100
voidzero-dev/void#562 ·
維護者通常 1 天內回覆
查看 voidzero-dev/void 的全部 Issue
相似的 Issue
-
level/task type/bug
難度 2/5 1-3 小時 新手友好度 75/100
wazuh/wazuh-dashboard-plugins#9508 ·
維護者通常 1 天內回覆
-
Testnet EOL page links Zonescan mainnet可能已有人在做 @napelvs 今天認領。 未關閉
難度 1/5 1 小時以內 新手友好度 76/100
-
難度 2/5 1-3 小時 新手友好度 78/100
Azure-Samples/azure-search-openai-demo#3249 ·
維護者通常 1 天內回覆
-
[Docs] docs/architecture.mermaid leaves out 21 of the 51 src/ directories可能已有人在做 @uyg7x 今天認領。 未關閉
難度 2/5 1-3 小時 新手友好度 75/100
維護者通常 1 天內回覆
-
enhancement status:needs-triage
難度 2/5 1-3 小時 新手友好度 68/100
dongdongbh/Mindwtr#1438 ·
維護者通常 1 天內回覆