Skip to content

Commit 2b67c91

Browse files
Sync docs from wheels@4ae7eae
1 parent c1671da commit 2b67c91

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

docs/3.1.0/guides/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)
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
---
2+
description: Use middleware to process requests before they reach your controllers — add headers, enforce CORS, reject unauthorized requests, and more.
3+
---
4+
5+
# Middleware
6+
7+
Middleware lets you intercept and modify requests at the **dispatch level**, before a controller is even instantiated. This is different from [filters](using-filters.md), which run inside a specific controller.
8+
9+
Use middleware when you need logic that applies globally or to groups of routes — things like request IDs, CORS headers, security headers, rate limiting, or authentication gates.
10+
11+
## How It Works
12+
13+
Each middleware implements a simple contract: receive a `request` struct and a `next` function, do your work, then call `next(request)` to pass control down the chain. The last step in the chain is the actual controller dispatch.
14+
15+
```
16+
Request → RequestId → Cors → SecurityHeaders → Controller → Response
17+
18+
next(request)
19+
```
20+
21+
This is sometimes called the "onion model" — the first middleware registered is the outermost layer.
22+
23+
## The Middleware Interface
24+
25+
Every middleware component must implement `wheels.middleware.MiddlewareInterface`:
26+
27+
```cfm
28+
// vendor/wheels/middleware/MiddlewareInterface.cfc
29+
interface {
30+
public string function handle(required struct request, required any next);
31+
}
32+
```
33+
34+
- **`request`** — A struct containing `params`, `route`, `pathInfo`, and `method`, plus any data added by prior middleware.
35+
- **`next`** — A closure. Call `next(request)` to continue to the next middleware (or the controller). Skip it to short-circuit the pipeline.
36+
- **Return value** — The response string.
37+
38+
## Registering Global Middleware
39+
40+
Register middleware in `config/settings.cfm`. They run on **every request** in the order listed:
41+
42+
```cfm
43+
// config/settings.cfm
44+
set(
45+
middleware = [
46+
new wheels.middleware.RequestId(),
47+
new wheels.middleware.SecurityHeaders(),
48+
new wheels.middleware.Cors(allowOrigins="https://myapp.com")
49+
]
50+
);
51+
```
52+
53+
You can pass component instances (as above) or CFC dot-paths as strings:
54+
55+
```cfm
56+
set(middleware = ["wheels.middleware.RequestId", "app.middleware.RateLimiter"]);
57+
```
58+
59+
String paths are instantiated automatically with a no-arg `init()`.
60+
61+
## Route-Scoped Middleware
62+
63+
Attach middleware to specific route groups using the `middleware` argument on `scope()`:
64+
65+
```cfm
66+
// config/routes.cfm
67+
mapper()
68+
.scope(path="/api", middleware=[new app.middleware.ApiAuth()])
69+
.resources("users")
70+
.resources("products")
71+
.end()
72+
.resources("pages") // no ApiAuth middleware here
73+
.end();
74+
```
75+
76+
Route-scoped middleware runs **after** global middleware. Scopes nest — child scopes inherit and append to parent middleware:
77+
78+
```cfm
79+
mapper()
80+
.scope(path="/api", middleware=["app.middleware.ApiAuth"])
81+
.scope(path="/admin", middleware=["app.middleware.AdminOnly"])
82+
.resources("settings") // runs: global → ApiAuth → AdminOnly
83+
.end()
84+
.resources("users") // runs: global → ApiAuth
85+
.end()
86+
.end();
87+
```
88+
89+
## Built-in Middleware
90+
91+
Wheels ships with three middleware components you can use immediately.
92+
93+
### RequestId
94+
95+
Assigns a unique UUID to every request for tracing and debugging.
96+
97+
- Sets `request.wheels.requestId`
98+
- Adds `X-Request-Id` response header
99+
100+
```cfm
101+
set(middleware = [new wheels.middleware.RequestId()]);
102+
```
103+
104+
### SecurityHeaders
105+
106+
Adds OWASP-recommended security headers to every response.
107+
108+
| Header | Default |
109+
|--------|---------|
110+
| `X-Frame-Options` | `SAMEORIGIN` |
111+
| `X-Content-Type-Options` | `nosniff` |
112+
| `X-XSS-Protection` | `1; mode=block` |
113+
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
114+
115+
```cfm
116+
// Use defaults
117+
set(middleware = [new wheels.middleware.SecurityHeaders()]);
118+
119+
// Customize
120+
set(middleware = [
121+
new wheels.middleware.SecurityHeaders(
122+
frameOptions = "DENY",
123+
referrerPolicy = "no-referrer"
124+
)
125+
]);
126+
```
127+
128+
Set any parameter to an empty string to disable that header.
129+
130+
### Cors
131+
132+
Handles Cross-Origin Resource Sharing headers and OPTIONS preflight requests.
133+
134+
```cfm
135+
set(middleware = [
136+
new wheels.middleware.Cors(
137+
allowOrigins = "https://myapp.com,https://admin.myapp.com",
138+
allowMethods = "GET,POST,PUT,DELETE",
139+
allowHeaders = "Content-Type,Authorization",
140+
allowCredentials = true,
141+
maxAge = 86400
142+
)
143+
]);
144+
```
145+
146+
| Parameter | Default | Description |
147+
|-----------|---------|-------------|
148+
| `allowOrigins` | `"*"` | Comma-delimited list of allowed origins |
149+
| `allowMethods` | `"GET,POST,PUT,PATCH,DELETE,OPTIONS"` | Allowed HTTP methods |
150+
| `allowHeaders` | `"Content-Type,Authorization,X-Requested-With"` | Allowed request headers |
151+
| `allowCredentials` | `false` | Whether to allow cookies/auth headers |
152+
| `maxAge` | `86400` | Preflight cache duration in seconds |
153+
154+
For simple CORS needs, you may prefer the existing [CORS Requests](cors-requests.md) guide which covers header-only approaches.
155+
156+
## Writing Custom Middleware
157+
158+
Create a CFC that implements `wheels.middleware.MiddlewareInterface`:
159+
160+
```cfm
161+
// app/middleware/RateLimiter.cfc
162+
component implements="wheels.middleware.MiddlewareInterface" output="false" {
163+
164+
public RateLimiter function init(numeric maxRequests = 100) {
165+
variables.maxRequests = arguments.maxRequests;
166+
return this;
167+
}
168+
169+
public string function handle(required struct request, required any next) {
170+
// Check rate limit (pseudo-code)
171+
local.clientIp = cgi.remote_addr;
172+
if ($isRateLimited(local.clientIp)) {
173+
cfheader(statusCode="429", statusText="Too Many Requests");
174+
return "Rate limit exceeded. Try again later.";
175+
}
176+
177+
// Continue to next middleware / controller
178+
return arguments.next(arguments.request);
179+
}
180+
181+
private boolean function $isRateLimited(required string ip) {
182+
// Your rate limiting logic here
183+
return false;
184+
}
185+
186+
}
187+
```
188+
189+
### Patterns
190+
191+
**Enrich the request** — add data for downstream middleware or the controller:
192+
193+
```cfm
194+
public string function handle(required struct request, required any next) {
195+
arguments.request.currentUser = $lookupUser();
196+
return arguments.next(arguments.request);
197+
}
198+
```
199+
200+
**Short-circuit** — return a response without calling `next()`:
201+
202+
```cfm
203+
public string function handle(required struct request, required any next) {
204+
if (!$isAuthenticated(arguments.request)) {
205+
cfheader(statusCode="401");
206+
return "Unauthorized";
207+
}
208+
return arguments.next(arguments.request);
209+
}
210+
```
211+
212+
**Post-process** — modify the response after the controller runs:
213+
214+
```cfm
215+
public string function handle(required struct request, required any next) {
216+
local.startTick = GetTickCount();
217+
local.response = arguments.next(arguments.request);
218+
local.elapsed = GetTickCount() - local.startTick;
219+
cfheader(name="X-Response-Time", value="#local.elapsed#ms");
220+
return local.response;
221+
}
222+
```
223+
224+
## Middleware vs. Filters
225+
226+
| | Middleware | Filters |
227+
|---|---|---|
228+
| **Runs at** | Dispatch level (before controller) | Controller level (inside controller) |
229+
| **Scope** | Global or route group | Single controller |
230+
| **Access to** | Request struct, response string | Controller instance, `params`, views |
231+
| **Best for** | Cross-cutting concerns (auth, headers, logging) | Controller-specific logic (load record, check permissions) |
232+
233+
Both can coexist. A typical setup might use middleware for security headers and request IDs globally, then filters for loading specific records inside individual controllers.

0 commit comments

Comments
 (0)