You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An action's declared export const middleware = [...] runs automatically on the RPC boundary, but the route() adapter does NOT apply it. route(action) runs the action body with no guards. Found by dogfooding a real saas app while cross-checking blog claims.
AGENTS.md and the (now corrected) per-action-middleware blog both state middleware "runs on BOTH the RPC and route.ts (including the route() adapter) boundaries". That is false for route().
Reproduction (verified)
Action with export const middleware = [setUser, block] where block returns { success:false, error:'mw-blocked' } without calling next():
route() boundary (export const POST = route(guarded)) -> {"success":true,"data":{"ranBody":true}} (middleware skipped, body ran)
Root cause
node_modules/@webjsdev/server/src/action-route.js around L127-132: const middleware = opts.middleware || []; then runActionChain(middleware, ...). The adapter only runs middleware passed explicitly via opts.middleware; it never reads the action's own config export. readActionConfig / the middleware reserved export (action-config.jsRESERVED_CONFIG) is consulted on the RPC boundary but not by route().
Design / approach
Two acceptable resolutions (pick one, update the other surfaces to match):
Make route() honor the action's declared middleware by default (read the action's export const middleware and merge with opts.middleware), so the "declare once, protected on every entry point" guarantee actually holds. This is the stronger product outcome. The same likely applies to export const validate (verify whether route() auto-applies the declared validator or also requires opts.validate).
Keep route() explicit and fix the docs: AGENTS.md ("HTTP-verb actions via config exports" + per-action middleware section) must stop claiming route() auto-applies declared middleware.
If choosing option 1: also decide precedence when BOTH the declared middleware and opts.middleware are present (append? opts override?). Check export const validate for the same gap and fix consistently.
Landmine: the action module's config is read statically elsewhere for the RPC path; reuse that reader rather than re-parsing.
Docs surfaces to sync (option 2, or after option 1): AGENTS.md, agent-docs/recipes.md, docs site server-actions page. Blog per-action-middleware.md (in PR docs: add 12 SEO blog posts for shipped-but-unblogged features #875) was already corrected to describe current behavior.
Acceptance criteria
route(action) behavior for a declared export const middleware matches the docs (either auto-applied, or docs updated to require route(action, { middleware }))
Same decision applied to export const validate
Test at the route()/server layer proving the chosen behavior, incl. the counterfactual
AGENTS.md + agent-docs + docs site consistent with the chosen behavior
Problem
An action's declared
export const middleware = [...]runs automatically on the RPC boundary, but theroute()adapter does NOT apply it.route(action)runs the action body with no guards. Found by dogfooding a real saas app while cross-checking blog claims.AGENTS.md and the (now corrected) per-action-middleware blog both state middleware "runs on BOTH the RPC and
route.ts(including theroute()adapter) boundaries". That is false forroute().Reproduction (verified)
Action with
export const middleware = [setUser, block]whereblockreturns{ success:false, error:'mw-blocked' }without callingnext():POST /__webjs/action/<hash>/guarded) ->{"success":false,"error":"mw-blocked"}(middleware ran, correct)route()boundary (export const POST = route(guarded)) ->{"success":true,"data":{"ranBody":true}}(middleware skipped, body ran)Root cause
node_modules/@webjsdev/server/src/action-route.jsaround L127-132:const middleware = opts.middleware || [];thenrunActionChain(middleware, ...). The adapter only runs middleware passed explicitly viaopts.middleware; it never reads the action's own config export.readActionConfig/ themiddlewarereserved export (action-config.jsRESERVED_CONFIG) is consulted on the RPC boundary but not byroute().Design / approach
Two acceptable resolutions (pick one, update the other surfaces to match):
route()honor the action's declared middleware by default (read the action'sexport const middlewareand merge withopts.middleware), so the "declare once, protected on every entry point" guarantee actually holds. This is the stronger product outcome. The same likely applies toexport const validate(verify whetherroute()auto-applies the declared validator or also requiresopts.validate).route()explicit and fix the docs: AGENTS.md ("HTTP-verb actions via config exports" + per-action middleware section) must stop claiming route() auto-applies declared middleware.Implementation notes (for the implementing agent)
packages/server/src/action-route.js(theroute()adapter, ~L120-140),packages/server/src/action-config.js(readActionConfig,RESERVED_CONFIG),packages/server/src/action-middleware.js(runActionChain).opts.middlewareare present (append? opts override?). Checkexport const validatefor the same gap and fix consistently.agent-docs/recipes.md, docs site server-actions page. Blogper-action-middleware.md(in PR docs: add 12 SEO blog posts for shipped-but-unblogged features #875) was already corrected to describe current behavior.Acceptance criteria
route(action)behavior for a declaredexport const middlewarematches the docs (either auto-applied, or docs updated to requireroute(action, { middleware }))export const validate