# Design: Middleware
**Author**: gak
**Status**: draft / ==in-review== / accepted / implemented / rejected
## Description
Middleware, interceptors, hooks, decorators, wrappers are different terms describing this functionality.
It is a way to have some code be automatically run before and after a selected set of verbs, as if they were written inside the verb function.
You can apply middleware to verbs across the project, just a module, or specific verbs.
## Motivation
Reducing repetitive boilerplate code and copy pasta bugs.
Potential uses:
- Sanitization
- Serialization
- Auth
- Encryption
- Tracing
- Performance
- Logging
- Error reporting
- Rate limiting
- Features/AB testing
## Goals
- Apply at different scopes: project, module, verb.
- Stack multiple middleware, in a clear stack order:
- Biggest scope first
- No ordering overriding (yet).
- Middleware options/arguments (eg a group that is allowed) that can be set differently to different verbs/scopes.
These are common for middleware, however are good to have explicitly listed:
- Pass through original response values.
- Error handling:
- Ability to abort the chain of middleware before or after the verb is called.
- Ability to handle errors from a verb (or other middleware), and continue the chain without error.
- Modify/replace argument content.
- Modify/replace return content.
### Non-Goals
- Middleware to not apply to async calls.
- Regex scopes:
- Because now we have 2 problems, and there is little reason to use it when you can just add them to each verb explicitly.
- Type scopes. (Later e.g. via `//ftl:onlyApplyToRequestType HttpRequest[Any]`)
- Accept generic types.
- "binary middleware" to handle []byte rather than the verb type.
- We don't want to expose internal FTL data.
## Design
Middleware functions are just verbs, however (statically checked) they:
- ~~Must call the "next" verb in the chain via `ftl.CallNext(ctx, in.In)`.~~ Adds extra work for little benefit, e.g. if the call is not actually in the method body.
- Must use the type `MiddlewareIn[O]` where `O` describes middleware options.
This is declared by FTL to be used as the middleware verb argument:
```go
type MiddlewareIn[O any] struct {
/// In is the first argument of the wrapped verb, in JSONish values.
/// `map[string]interface{}`?
In any
// Options that can be passed into the middleware.
// Use Unit for no args.
Options O
// Verb that is being being wrapped.
Verb ftl.Ref
// InRef is the module/name of the input type
InRef ftl.Ref
// OutRef is the module/name of the output type
OutRef ftl.Ref
}
```
### Simple example
Define the middleware:
```go
// EnterpriseDelay adds a few seconds to this verb.
//ftl:verb
func EnterpriseDelay(ctx context.Context, in MiddlewareIn[Unit]) (any, error) {
// This will let us charge more $.
time.Sleep(2 * time.Second)
// Call the next middleware in the chain or finally the wrapped verb.
out, err := ftl.CallNext(ctx, in.In)
if err != nil {
// An error in the chain or wrapped verb, pass it along.
return nil, fmt.Errorf("enterprise delay middleware: %s %w", in.Verb, err)
}
return out, nil
}
```
Use the middleware on this verb:
```go
//ftl:verb
//ftl:middleware EnterpriseDelay
func Echo(ctx context.Context, in EchoRequest) (EchoResponse, error) {
// ...
}
```
### Complex ingress example
```go
// AllowOptions defines what roles a verb can run with.
type AllowOptions struct {
Roles []string
}
//ftl:verb
func AllowMiddleware(ctx context.Context, in MiddlewareIn[AllowOptions]) (any, error) {
if len(in.Options.Roles) == 0 {
return nil, fmt.Errorf("roles is a required option in AllowMiddlware")
}
// Preprocessing logic before the verb will be called.
authz, ok := in.In["headers"]["Authorization"]
if !ok { return nil, ... }
db := db.Get(ctx)
if !db.AuthAllowed(authz, in.Options.Roles) {
// Early exit due to middleware check.
return nil, ...
}
// Call the next middleware in the chain or finally the wrapped verb.
out, err := ftl.CallNext(ctx, in.In)
if err != nil {
// An error in the chain or wrapped verb, pass it along.
return nil, fmt.Errorf("allow middleware: %s %w", in.Verb, err)
}
// Postprocessing code after the verb has been called.
logging.Debugf("Verb %s finished calling after giving %s access.", in.Verb, options.Role)
// Modify the verbs return value if required.
out["headers"]["X-granted"] = fmt.Sprintf("%s", in.Options.Roles)
// Pass on the successful and modified output from the chain or wrapped verb.
return out, nil
}
```
Using the middleware:
```go
type User struct{}
type CreateUserResponse struct {
id string
}
//ftl:ingress http POST /user
//ftl:middleware AllowMiddleware(roles=["user:create"])
func CreateUser(ctx context.Context, req builtin.HttpRequest[User]) (builtin.HttpResponse[CreateUserResponse, ErrorResponse], error) {
return builtin.HttpResponse[CreateUserResponse, string]{
Body: ftl.Some(CreateUserResponse {
id: new_user(ctx, req.Body)
}),
}, nil
}
### Schema
module one {
// uses ... // not this, put all of the middleware above the verb
export verb authzMiddleware(builtin.Middleware[Authz]) Out
export verb one(Empty) Empty
middleware
two.errorReporter,
one.authzMiddleware("admin"),
one.cancelMillis(timeout=250, etc="hi"),
one.sanitizedLogger("password"),
// TODO change this too?
+calls
two.two,
three.three,
}
```
### Scopes
Verb level scopes can be defined at the verb itself with:
```
//ftl:middleware [MiddlewareVerb(args)]
```
Otherwise for global and module level scopes, define them in `ftl-project.toml`:
```toml
name = "awesome"
module-dirs = ["time", "bingo"]
ftl-min-version = ""
[global]
middleware = [
"time.EnterpriseDelay(2000)",
"bingo.BingoMiddleware",
]
[global.configuration]
key = "inline://InZhbHVlIg"
[modules]
[modules.bingo]
milddleware = [
"bingo.RandomlyFail",
]
```
### Required changes
- `//ftl:middleware` directive.
- ~~Static check for `ftl.CallNext()` if used as a middleware.~~
- Static check for `//ftl:middleware(options)` is calling middleware options correctly.
- Only apply to sync verbs:
- Verbs
- Ingress
- Sink
- Scopes:
- Global and module scopes defined in ftl-project.toml
- Verb "middleware" scope.
- Ordering from global first, then module, finally verb level (`//ftl:middleware`).
- Schema syntax changes:
- "middleware"
- Change "+calls" to line up arguments similar to middlware for consistency.
- Chain middleware when a verb is called via `ftl.CallNext()`.
- Check for middleware before a verb call, call middleware instead while keeping track of the chain and return.
- This might need some DB state otherwise the request itself could contain info on the chain state?
- Follow schema order of chained middleware to be called init `ftl.CallNext()``
- And finally `ftl.CallNext()` to call the verb when there is no middleware left.
## Rejected Alternatives
### Extra argument for options
```go
//ftl:middleware
func PerformanceTimer(ctx Context, q Q, timeout int) (A, error) {}
```
Doesn't line up with a standard verb signature.
### Python decorator style
```go
//ftl:verb
func PerformanceTimer(ctx ctx.Context, options ...) (func(...), error) {
var verb := ...
return func(ctx: context.Context, req Q) (A, error) error {
var logger = ctx.GetLogger()
var start = time.Now()
a, err := f(q)
var taken = time.Now() - start
logger.Debug("Taken %s", taken)
if taken > time.Second {
return A{}, fmt.Errorf("Operation took too long in function %s and argument %s", f, q)
}
return a, err
}
}
```
Overly complex.
### Generics
Generics added too much complexity to both the user verb and internal code, which lead to the `InMiddleware` struct.