Skip to content

dogfood: route() adapter ignores an action's declared export const middleware #876

Description

@vivek7405

Problem

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():

  • RPC boundary (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.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.js RESERVED_CONFIG) is consulted on the RPC boundary but not by route().

Design / approach

Two acceptable resolutions (pick one, update the other surfaces to match):

  1. 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).
  2. 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.

Implementation notes (for the implementing agent)

  • Where: packages/server/src/action-route.js (the route() adapter, ~L120-140), packages/server/src/action-config.js (readActionConfig, RESERVED_CONFIG), packages/server/src/action-middleware.js (runActionChain).
  • 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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions