Skip to content

Commit 5d1b8fd

Browse files
committed
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.
1 parent 12b34db commit 5d1b8fd

3 files changed

Lines changed: 10 additions & 9 deletions

File tree

.agents/skills/webjs/references/muscle-memory-gotchas.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ Export `GET` / `POST` / etc. as named async functions `(request, { params }) =>
202202

203203
### `middleware.ts` is per-segment and chainable, not one matcher config
204204

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.
206206

207207
### No `<Link>`, no `next/navigation`, no `next/*` libraries
208208

website/app/docs/architecture/page.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,21 @@ import { listPosts } from '#modules/posts/queries/list-posts.server.ts';</code-b
102102
103103
<h2>Request Lifecycle</h2>
104104
<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>
108109
<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/&lt;hash&gt;/&lt;fn&gt;</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/&lt;hash&gt;/&lt;fn&gt;</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>
110111
<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>
112113
<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>
114114
<li><strong>Response</strong> is sent (with compression in prod and cache headers).</li>
115115
</ol>
116116
117117
<h2>Progressive Enhancement</h2>
118118
<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>&lt;a&gt;</code> links, and handles <code>&lt;form&gt;</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>&lt;a&gt;</code> links, and handles <code>&lt;form&gt;</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.
120120
</p>
121121
<ul>
122122
<li><strong>Read-paths:</strong> the SSR'd HTML is the user's first interaction. With JS disabled, content reads, <code>&lt;a&gt;</code> links navigate, and display-only custom elements render correctly.</li>

website/app/docs/middleware/page.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export default function Middleware() {
99
1010
<h2>Root Middleware</h2>
1111
<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>
1314
<code-block>my-app/
1415
middleware.ts # root middleware: runs on every app request
1516
app/

0 commit comments

Comments
 (0)