Files
Flash5/flash-extensions/flash-ext-limiter/docs/guard.md
T

145 lines
4.4 KiB
Markdown

# Guard — programmatic rate limiting for lambda routes
`Guard` is the rate-limit API for lambda (inline) route registrations. It produces
a `Middleware` that is composed once at route-wiring time — the resolver lambda is
captured directly into the closure, with no map lookup on the request hot-path.
## Obtaining Guard
`Guard` is provided in the `FlashContext` after `LimiterExtension` is installed:
```java
Guard guard = app.ctx().require(Guard.class);
```
Or inside another extension:
```java
public void install(FlashRegistrar app, FlashContext ctx) {
Guard guard = ctx.require(Guard.class);
// ...
}
```
## API
```java
// Fixed window (default strategy)
Middleware limit(String resolverKey, int requests, long window, TimeUnit unit)
// Explicit strategy
Middleware limit(String resolverKey, int requests, long window, TimeUnit unit, LimitStrategy strategy)
```
Both overloads:
- Resolve the named key lambda **once** at call time (fail-fast if unknown).
- Return a stateless `Middleware` whose closure captures the lambda and `LimitConfig` directly.
- Share the `BucketStore` with all other rules registered through this `LimiterExtension` instance.
## Examples
### Simple per-IP limit on a lambda route
```java
Guard guard = app.ctx().require(Guard.class);
app.get("/api/search", (req, res) -> searchService.query(req.query("q")))
.with(guard.limit("ip", 100, 1, TimeUnit.SECONDS));
```
### Per authenticated user — token bucket
```java
app.post("/api/export", (req, res) -> exportService.run(req))
.with(guard.limit("auth_user", 10, 1, TimeUnit.MINUTES, LimitStrategy.TOKEN_BUCKET));
```
### Chaining with other middleware
`Guard.limit(...)` returns a plain `Middleware`, so it composes with `Middleware.of()`
and `.andThen()` exactly like any other middleware:
```java
Middleware secured = Middleware.of(
guard.limit("ip", 200, 1, TimeUnit.SECONDS), // ← outermost: runs first
oidc.protect()
);
app.get("/dashboard", handler).with(secured);
```
Or with `.andThen()` for two middlewares:
```java
app.get("/dashboard", handler)
.with(guard.limit("ip", 200, 1, TimeUnit.SECONDS).andThen(oidc.protect()));
```
### Different limits on the same path by method
```java
// Read: 500/s; Write: 20/s
app.get("/api/items", readHandler) .with(guard.limit("ip", 500, 1, TimeUnit.SECONDS));
app.post("/api/items", writeHandler).with(guard.limit("ip", 20, 1, TimeUnit.SECONDS));
```
Each `.with(guard.limit(...))` call creates an independent bucket store key namespace —
GET and POST requests to `/api/items` share the same IP bucket only if you share the same
`Middleware` instance. Using two `guard.limit(...)` calls creates **two independent buckets**.
### Reusing a middleware instance across routes
To share a single bucket pool across multiple routes (treating them as one combined limit):
```java
Middleware sharedIpLimit = guard.limit("ip", 1000, 1, TimeUnit.MINUTES);
app.get("/api/items", handler1).with(sharedIpLimit);
app.get("/api/items/{id}", handler2).with(sharedIpLimit);
app.post("/api/items", handler3).with(sharedIpLimit);
```
All three routes now draw from the same per-IP bucket — 1000 combined requests per minute.
### Inside an extension
```java
public class MyApiExtension implements FlashExtension {
public void install(FlashRegistrar app, FlashContext ctx) {
Guard guard = ctx.require(Guard.class); // LimiterExtension must be installed first
Middleware ipLimit = guard.limit("ip", 60, 1, TimeUnit.SECONDS);
app.get("/api/status", statusHandler) .with(ipLimit);
app.get("/api/metrics", metricsHandler).with(ipLimit);
}
}
```
### Large window
```java
app.get("/api/export", exportHandler)
.with(guard.limit("api_key", 50, 24, TimeUnit.HOURS));
```
## Fail-fast
If the resolver name is not registered, `guard.limit(...)` throws immediately
(at wiring time, not at request time):
```
InitializationException: Rate-limit resolver "auth_user" is not registered.
```
## Comparison: Guard vs @Limit
| | `@Limit` | `Guard.limit(...)` |
|---|---|---|
| Route style | Class-based `RequestHandler` | Lambda `(req, res) -> ...` |
| Configuration | Annotation fields | Method arguments |
| Where resolved | `AnnotationProcessor` at `scan()` | `guard.limit(...)` call at wiring |
| Hot-path overhead | Zero | Zero |
| Fail-fast | Yes | Yes |
| Composable with `Middleware.of()` | Via annotation processor order | Yes, directly |