---
# System prepended metadata

title: Middleware
tags: [in-review]

---

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