Skip to content

Commit 4ae7eae

Browse files
authored
Merge pull request #1924 from wheels-dev/peter/middleware-pipeline-1906
2 parents 148ff79 + 81996ad commit 4ae7eae

File tree

17 files changed

+944
-9
lines changed

17 files changed

+944
-9
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Middleware Pipeline
2+
3+
## Architecture
4+
5+
Middleware runs at the dispatch level in `Dispatch.cfc`, wrapping controller execution. The pipeline uses nested closures (onion model) — first registered = outermost layer.
6+
7+
```
8+
Dispatch.$request()
9+
→ $buildMiddlewarePipeline() # on $init(), from application.wheels.middleware
10+
→ $getRouteMiddleware(params) # per-request, from matched route
11+
→ Pipeline.run(request, coreHandler)
12+
→ middleware[1].handle(request, next)
13+
→ middleware[2].handle(request, next)
14+
→ coreHandler(request) # controller() + processAction()
15+
```
16+
17+
## Key Files
18+
19+
| File | Purpose |
20+
|------|---------|
21+
| `vendor/wheels/middleware/MiddlewareInterface.cfc` | Contract: `handle(request, next)` |
22+
| `vendor/wheels/middleware/Pipeline.cfc` | Chains middleware via nested closures |
23+
| `vendor/wheels/middleware/RequestId.cfc` | Adds `X-Request-Id` header + `request.wheels.requestId` |
24+
| `vendor/wheels/middleware/Cors.cfc` | CORS headers + OPTIONS preflight |
25+
| `vendor/wheels/middleware/SecurityHeaders.cfc` | OWASP security headers |
26+
| `vendor/wheels/Dispatch.cfc` | `$buildMiddlewarePipeline()`, `$getRouteMiddleware()`, modified `$request()` |
27+
| `vendor/wheels/mapper/scoping.cfc` | `middleware` param on `scope()`, parent-child merging |
28+
| `vendor/wheels/mapper/matching.cfc` | Copies `middleware` from scope stack to matched route |
29+
| `vendor/wheels/events/onapplicationstart.cfc` | Initializes `application.$wheels.middleware = []` |
30+
31+
## Pipeline.cfc Internals
32+
33+
`Pipeline.run(request, coreHandler)` iterates middleware in reverse, wrapping each around the next handler:
34+
35+
```cfm
36+
// Build chain from inside out
37+
local.next = arguments.coreHandler;
38+
for (local.i = ArrayLen(variables.middleware); local.i >= 1; local.i--) {
39+
local.next = $wrapMiddleware(variables.middleware[local.i], local.next);
40+
}
41+
return local.next(arguments.request);
42+
```
43+
44+
**CFML closure scoping gotcha:** `$wrapMiddleware` uses a shared `var ctx = {}` struct because closures in CFML have their own `local` scope. Writing `local.mw` inside a closure creates a new variable, not a reference to the enclosing function's `local.mw`.
45+
46+
```cfm
47+
private any function $wrapMiddleware(required any mw, required any nextFn) {
48+
var ctx = {mw = arguments.mw, nextFn = arguments.nextFn};
49+
return function(required struct request) {
50+
return ctx.mw.handle(request = arguments.request, next = ctx.nextFn);
51+
};
52+
}
53+
```
54+
55+
## Registration
56+
57+
### Global (config/settings.cfm)
58+
59+
```cfm
60+
set(middleware = [
61+
new wheels.middleware.RequestId(),
62+
new wheels.middleware.SecurityHeaders(),
63+
new wheels.middleware.Cors(allowOrigins="https://myapp.com")
64+
]);
65+
```
66+
67+
Accepts instances or string CFC paths (auto-instantiated with `init()`).
68+
69+
### Route-scoped (config/routes.cfm)
70+
71+
```cfm
72+
mapper()
73+
.scope(path="/api", middleware=["app.middleware.ApiAuth"])
74+
.resources("users")
75+
.end()
76+
.end();
77+
```
78+
79+
Route middleware runs after global middleware. Nested scopes inherit parent middleware.
80+
81+
## Dispatch.cfc Integration
82+
83+
In `$request()`, the controller dispatch is wrapped in a `coreHandler` closure:
84+
85+
```cfm
86+
local.coreHandler = function(required struct request) {
87+
local.ctrl = controller(name=request.params.controller, params=request.params);
88+
local.ctrl.processAction();
89+
if (local.ctrl.$performedRedirect()) {
90+
$location(argumentCollection=local.ctrl.getRedirect());
91+
}
92+
local.ctrl.$flashClear();
93+
return local.ctrl.response();
94+
};
95+
```
96+
97+
If route-scoped middleware exists, a merged pipeline (global + route) is created per-request. Otherwise the pre-built global pipeline is used.
98+
99+
## Request Context Struct
100+
101+
The `request` struct passed through middleware contains:
102+
103+
| Key | Description |
104+
|-----|-------------|
105+
| `params` | Merged URL/form/route params (same as controller `params`) |
106+
| `route` | The matched route struct from `request.wheels.currentRoute` |
107+
| `pathInfo` | Raw path info string |
108+
| `method` | HTTP method (GET, POST, etc.) |
109+
110+
Middleware can add arbitrary keys (e.g., `request.currentUser`) for downstream access.
111+
112+
## Built-in Middleware Reference
113+
114+
### RequestId
115+
- No constructor args
116+
- Sets `request.wheels.requestId = CreateUUID()`
117+
- Adds `X-Request-Id` response header
118+
119+
### Cors
120+
- `allowOrigins` (default `"*"`) — comma-delimited origins
121+
- `allowMethods` (default `"GET,POST,PUT,PATCH,DELETE,OPTIONS"`)
122+
- `allowHeaders` (default `"Content-Type,Authorization,X-Requested-With"`)
123+
- `allowCredentials` (default `false`)
124+
- `maxAge` (default `86400`) — preflight cache seconds
125+
- Short-circuits on OPTIONS with empty response
126+
127+
### SecurityHeaders
128+
- `frameOptions` (default `"SAMEORIGIN"`)
129+
- `contentTypeOptions` (default `"nosniff"`)
130+
- `xssProtection` (default `"1; mode=block"`)
131+
- `referrerPolicy` (default `"strict-origin-when-cross-origin"`)
132+
- Set any to `""` to disable that header
133+
- Runs after `next()` (post-processing pattern)

CLAUDE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,30 @@ model("User").findInBatches(batchSize=500, callback=function(users) {
254254
model("User").active().findEach(batchSize=500, callback=function(user) { /* ... */ });
255255
```
256256

257+
## Middleware Quick Reference
258+
259+
Middleware runs at the dispatch level, before controller instantiation. Each implements `handle(request, next)`.
260+
261+
```cfm
262+
// config/settings.cfm — global middleware (runs on every request)
263+
set(middleware = [
264+
new wheels.middleware.RequestId(),
265+
new wheels.middleware.SecurityHeaders(),
266+
new wheels.middleware.Cors(allowOrigins="https://myapp.com")
267+
]);
268+
```
269+
270+
```cfm
271+
// config/routes.cfm — route-scoped middleware
272+
mapper()
273+
.scope(path="/api", middleware=["app.middleware.ApiAuth"])
274+
.resources("users")
275+
.end()
276+
.end();
277+
```
278+
279+
Built-in: `wheels.middleware.RequestId`, `wheels.middleware.Cors`, `wheels.middleware.SecurityHeaders`. Custom middleware: implement `wheels.middleware.MiddlewareInterface`, place in `app/middleware/`.
280+
257281
## Routing Quick Reference
258282

259283
```cfm

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
* [Sending Email](handling-requests-with-controllers/sending-email.md)
133133
* [Responding with Multiple Formats](handling-requests-with-controllers/responding-with-multiple-formats.md)
134134
* [Using the Flash](handling-requests-with-controllers/using-the-flash.md)
135+
* [Middleware](handling-requests-with-controllers/middleware.md)
135136
* [Using Filters](handling-requests-with-controllers/using-filters.md)
136137
* [Verification](handling-requests-with-controllers/verification.md)
137138
* [Event Handlers](handling-requests-with-controllers/event-handlers.md)

0 commit comments

Comments
 (0)