`defineHandler(middleware, handler)` breaks page loaders and actions: 500 on GET, empty 204 after an action, validator types dropped
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 55/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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
- Ngôn ngữ chính
- TypeScript
- Star
- 580
- Fork
- 56
- Merge trung bình
- 8 giờ 36 phút
- Pull request đã merge (30 ngày)
- 110
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của voidzero-dev/void
-
Static Cloudflare deploy ignores `cloudflare.assets.html_handling`Có thể đã có người làm @0froq đã nhận 1 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
voidzero-dev/void#580 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`void/log` serializes `Error` fields as `{}`, losing message and stackCó thể đã có người làm @RaphaelFakhri đã nhận 1 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
voidzero-dev/void#577 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
vp build / void deploy exit 0 when a page fails to prerender, and the page is silently missingĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
voidzero-dev/void#576 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Dev: `better-auth/client/plugins` fails to resolve when auth is enabled (the `better-auth/client` alias matches it as a prefix)Có thể đã có người làm @MaxtuneLee đã nhận 2 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
voidzero-dev/void#565 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
prepareDeployArtifact omits worker.vars that the CLI managed deploy includesCó thể đã có người làm @dvkam đã nhận 1 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
voidzero-dev/void#529 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của voidzero-dev/void
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
grafana/profiles-drilldown#1212 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
grafana/traces-drilldown#980 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Manager flashes an empty "errorOccurred" dialog while redirecting a logged-out user to loginĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
openremote/openremote#3487 · 1 bình luận ·
-
triage:needs-human
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
garrytan/gbrain#7635 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
coollabsio/jean#867 ·
Maintainer thường phản hồi trong vòng 1 ngày