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
docs: state the middleware bypass rule instead of a closed list
Third delta review found three more members of an enumeration I kept
asserting was complete. Every round has found another one, so this stops
listing and states the rule: anything the listener shell or the
pre-analysis stage answers bypasses root middleware, and everything routed
with the app reaches it. The examples stay, but as examples.
The two genuinely missing members are real bypasses. A WebSocket upgrade
bound for a route.ts exporting WS is intercepted at the server level on
both listener shells before the app handler is called, and it is
unambiguously an app route, which is the line the reworded "every app
request" was drawn to make. The dev SSE stream at /__webjs/events is
intercepted the same way, so the previous wording, which said the named
set was everything under /__webjs/* except the action endpoint, was wrong
in the direction where believing it costs you.
Inserting a step into the architecture lifecycle also shifted the list
under a "Step 6 above" reference further down the page, which now pointed
at segment middleware instead of the SSR pipeline. That reference is by
name now, so inserting a step cannot break it again. The lifecycle also
listed WebSocket upgrades as a late step, after middleware and route
matching, which the same interception contradicts.
Copy file name to clipboardExpand all lines: .agents/skills/webjs/references/muscle-memory-gotchas.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -202,7 +202,7 @@ Export `GET` / `POST` / etc. as named async functions `(request, { params }) =>
202
202
203
203
### `middleware.ts` is per-segment and chainable, not one matcher config
204
204
205
-
The file stays `middleware.ts`, NOT Next 16's renamed `proxy.ts`. WebJs middleware is in-process, chainable, and per-segment (the Remix / Koa model). There is no `export const config = { matcher }` and no single-file restriction. The default export is `async (req, next) => Response`: return a Response to short-circuit, or call `next()` and post-process. Colocate `app/admin/middleware.ts` next to the admin routes and it runs for that subtree only. An optional root `middleware.ts` runs on every app request, outermost to innermost. Two things are served ahead of it: the framework's own `/__webjs/*` assets and probes, in both modes, and in DEV only, `/public/*` plus the `/sw.js` / `/offline.html` root remaps and `/favicon.ico`, so a stylesheet is never queued behind the dev startup analysis. In production those static files go through root middleware normally, so a middleware that guards an asset still guards it where it counts.
205
+
The file stays `middleware.ts`, NOT Next 16's renamed `proxy.ts`. WebJs middleware is in-process, chainable, and per-segment (the Remix / Koa model). There is no `export const config = { matcher }` and no single-file restriction. The default export is `async (req, next) => Response`: return a Response to short-circuit, or call `next()` and post-process. Colocate `app/admin/middleware.ts` next to the admin routes and it runs for that subtree only. An optional root `middleware.ts` runs on every app request, outermost to innermost. Some requests are answered before it and never reach it, and the RULE is what to remember, not the list: anything the listener shell or the framework's pre-analysis stage answers bypasses root middleware, and everything routed with the app reaches it. That covers WebSocket upgrades bound for a `route.ts` exporting `WS`, the dev SSE stream at `/__webjs/events`, and the framework's own `/__webjs/*` runtime assets and probes; in DEV only it also covers `/public/*` plus the `/sw.js` / `/offline.html` root remaps and `/favicon.ico`, so a stylesheet is never queued behind the dev startup analysis. In production those static files go through root middleware normally. Server actions are routed with the app, so middleware DOES run for an action call, which is what lets you gate actions with auth or rate limiting.
206
206
207
207
### No `<Link>`, no `next/navigation`, no `next/*` libraries
Copy file name to clipboardExpand all lines: website/app/docs/architecture/page.ts
+7-7Lines changed: 7 additions & 7 deletions
Original file line number
Diff line number
Diff line change
@@ -102,21 +102,21 @@ import { listPosts } from '#modules/posts/queries/list-posts.server.ts';</code-b
102
102
103
103
<h2>Request Lifecycle</h2>
104
104
<ol>
105
-
<li><strong>HTTP request arrives</strong> at the Node HTTP server (or HTTP/2 if TLS configured).</li>
106
-
<li><strong>Some framework-internal assets and probes are answered here</strong>, ahead of everything below, because they depend on neither the app analysis nor the vendor importmap and a cold instance must not gate them: the health and readiness probes (<code>/__webjs/health</code>, <code>/__webjs/ready</code>), the build-info probe (<code>/__webjs/version</code>), the core runtime (<code>/__webjs/core/*</code>), the dev reload client and its SharedWorker (<code>/__webjs/reload.js</code>, <code>/__webjs/reload-worker.js</code>), and downloaded vendor bundles (<code>/__webjs/vendor/*</code>). This is a named set, not all of <code>/__webjs/*</code>: the server-action RPC endpoint is NOT in it (see step 5). In <strong>development only</strong>, <code>/public/*</code> plus the <code>/sw.js</code> and <code>/offline.html</code> root remaps and <code>/favicon.ico</code> are served here too, so a stylesheet is never queued behind the startup analysis.</li>
107
-
<li><strong>Root middleware</strong> (<code>middleware.ts</code>) runs next if present, for every request that was not already answered above.</li>
105
+
<li><strong>HTTP request arrives</strong> at the listener shell (<code>node:http</code>, or <code>Bun.serve</code> on Bun; HTTP/2 if TLS is configured).</li>
106
+
<li><strong>The listener shell answers what does not fit the request/response model</strong>, before the app handler is called at all: a <strong>WebSocket upgrade</strong> bound for a <code>route.ts</code> that exports <code>WS</code>, and in dev the live-reload <strong>SSE stream</strong> at <code>/__webjs/events</code>. Both are intercepted on the node and Bun shells alike.</li>
107
+
<li><strong>Framework-internal assets and probes are answered next</strong>, because they depend on neither the app analysis nor the vendor importmap and a cold instance must not gate them: the health, readiness and build-info probes (<code>/__webjs/health</code>, <code>/__webjs/ready</code>, <code>/__webjs/version</code>), the core runtime (<code>/__webjs/core/*</code>), the dev reload client and its SharedWorker (<code>/__webjs/reload.js</code>, <code>/__webjs/reload-worker.js</code>), and downloaded vendor bundles (<code>/__webjs/vendor/*</code>). Not everything under <code>/__webjs/*</code> is here: the server-action RPC endpoint is routed with the app, below. In <strong>development only</strong>, <code>/public/*</code> plus the <code>/sw.js</code> and <code>/offline.html</code> root remaps and <code>/favicon.ico</code> are served here too, so a stylesheet is never queued behind the startup analysis.</li>
108
+
<li><strong>Root middleware</strong> (<code>middleware.ts</code>) runs next if present, for every request not already answered above. The rule behind the exceptions is worth holding onto rather than the list: anything the listener shell or the pre-analysis stage answers never reaches your middleware, and everything routed with the app does.</li>
108
109
<li><strong>103 Early Hints</strong> sent (prod only) with modulepreload URLs for the matched page.</li>
109
-
<li><strong>Route matching</strong>: the router tries (in order) the server-action RPC endpoint (<code>/__webjs/action/<hash>/<fn></code>), static files, user source modules, API routes (<code>route.ts</code>), then page routes. The action endpoint sits here rather than in step 2, so root middleware DOES run for a server action, which is what lets you gate actions with auth or rate limiting. In production this is also where <code>/public/*</code> is served, so a middleware that guards an asset still guards it.</li>
110
+
<li><strong>Route matching</strong>: the router tries (in order) the server-action RPC endpoint (<code>/__webjs/action/<hash>/<fn></code>), static files, user source modules, API routes (<code>route.ts</code>), then page routes. The action endpoint is routed here rather than answered early, so root middleware DOES run for a server action, which is what lets you gate actions with auth or rate limiting. In production this is also where <code>/public/*</code> is served, so a middleware that guards an asset still guards it.</li>
110
111
<li><strong>Segment middleware</strong> chain runs (outermost → innermost) for the matched route.</li>
111
-
<li>For <strong>pages</strong>: SSR pipeline runs (load page + layouts, render to HTML, inject DSD, collect metadata, stream response with Suspense).</li>
112
+
<li>For <strong>pages</strong>: the SSR pipeline runs (load page + layouts, render to HTML, inject DSD, collect metadata, stream response with Suspense).</li>
112
113
<li>For <strong>API routes</strong>: the matched handler function runs, returns a Response.</li>
113
-
<li>For <strong>WebSocket upgrades</strong>: the WS handler is invoked with the ws object + Request.</li>
114
114
<li><strong>Response</strong> is sent (with compression in prod and cache headers).</li>
115
115
</ol>
116
116
117
117
<h2>Progressive Enhancement</h2>
118
118
<p>
119
-
Step 6 above produces real HTML. The SSR pipeline runs every web component's <code>render()</code> on the server, so the component's initial markup is in the response before any script loads. The browser paints content, processes <code><a></code> links, and handles <code><form></code> submissions before any JavaScript runs. The client router, custom-element upgrades, and Suspense streaming are <em>layered</em> on top of that HTML. They enhance an already-working page, they do not constitute it.
119
+
The SSR pipeline above produces real HTML. It runs every web component's <code>render()</code> on the server, so the component's initial markup is in the response before any script loads. The browser paints content, processes <code><a></code> links, and handles <code><form></code> submissions before any JavaScript runs. The client router, custom-element upgrades, and Suspense streaming are <em>layered</em> on top of that HTML. They enhance an already-working page, they do not constitute it.
120
120
</p>
121
121
<ul>
122
122
<li><strong>Read-paths:</strong> the SSR'd HTML is the user's first interaction. With JS disabled, content reads, <code><a></code> links navigate, and display-only custom elements render correctly.</li>
Copy file name to clipboardExpand all lines: website/app/docs/middleware/page.ts
+2-1Lines changed: 2 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -9,7 +9,8 @@ export default function Middleware() {
9
9
10
10
<h2>Root Middleware</h2>
11
11
<p>Place a <code>middleware.ts</code> at the root of your project (next to <code>app/</code>, not inside it). This middleware runs on <strong>every app request</strong> before WebJs routes it to a page, API route, or server action. Any of <code>middleware.ts</code>, <code>.js</code>, <code>.mts</code>, or <code>.mjs</code> works, and <code>.ts</code> wins if you somehow have more than one.</p>
12
-
<p>Two things are served ahead of it, so root middleware never sees them. The framework's own <code>/__webjs/*</code> assets and health probes bypass it in both dev and production, because they are framework infrastructure your app needs to boot rather than app routes. In <strong>development only</strong>, static files under <code>/public/*</code> (plus the <code>/sw.js</code> and <code>/offline.html</code> root remaps and <code>/favicon.ico</code>) are served ahead of it as well, so a stylesheet is never queued behind the dev server's startup analysis. In production those static files go through root middleware normally, so a middleware that protects an asset still protects it where it counts.</p>
12
+
<p>Some requests are answered before it and so never reach it. The rule is worth holding onto rather than a list: <strong>anything the listener shell or the framework's pre-analysis stage answers bypasses root middleware, and everything routed with your app reaches it.</strong> In practice that means WebSocket upgrades bound for a <code>route.ts</code> exporting <code>WS</code>, the dev live-reload SSE stream at <code>/__webjs/events</code>, and the framework's own <code>/__webjs/*</code> runtime assets and health probes, which your app needs in order to boot at all. In <strong>development only</strong>, static files under <code>/public/*</code> (plus the <code>/sw.js</code> and <code>/offline.html</code> root remaps and <code>/favicon.ico</code>) are answered there too, so a stylesheet is never queued behind the dev server's startup analysis.</p>
13
+
<p>Two things that look like exceptions are not. In production those <code>/public/*</code> files go through root middleware normally, so a middleware that protects an asset still protects it where it counts. And server actions are routed with your app, not answered early, so root middleware DOES run for an action call: that is what lets you gate actions with auth or rate limiting.</p>
13
14
<code-block>my-app/
14
15
middleware.ts # root middleware: runs on every app request
0 commit comments