Dependencies

Dependencies come in exactly two shapes. A guard validates and produces nothing. A provider produces a typed value. Both run before the handler, and both can refuse the request by returning an error.

Guards

A guard is func(ctx *muzak.Context) error.

// GetQueryToken is a guard dependency applied to the whole application.
//
// It rejects any request that does not carry a token query parameter. A guard
// produces no value; it either lets the request through or returns the error
// that becomes the response.
func GetQueryToken(ctx *muzak.Context) error {
    if ctx.Query("token") == "" {
        return muzak.BadRequest("token is required")
    }
    return nil
}

Attach it with WithDependencies, which works on an application, a router, or a single route.

// Every route in the application sits below this guard.
app := muzak.New(muzak.AppOptions{Title: "Awesome API"},
    muzak.WithDependencies(core.GetQueryToken))

// Only the admin subtree sits below this one.
app.Include(routers.Admin(),
    muzak.WithPrefix("/admin"),
    muzak.WithDependencies(core.GetTokenHeader(settings)))

// Only this route.
r.Post("/reindex", handlers.Reindex,
    muzak.WithDependencies(RequireMaintenanceWindow))

Guards run in declaration order, outermost first: those declared on the application run before those declared when including a router, which run before the route's own. The first guard to return an error stops the chain and produces the response.

Two shared-secret guards ship with the framework, both comparing in constant time:

muzak.WithDependencies(muzak.RequireBearerToken(settings.AdminToken))
muzak.WithDependencies(muzak.RequireHeaderToken("X-Token", "coneofsilence"))

They are meant for the shared-secret case, such as an internal service or a webhook receiver. Anything involving per-user credentials wants a provider that resolves the user instead.

Providers

A provider is func(ctx *muzak.Context) (T, error). Declare it with Needs and read the value with From.

// CurrentUser is the authenticated caller.
type CurrentUser struct {
    Username string
}

// GetCurrentUser resolves the caller from the Authorization header.
func GetCurrentUser(ctx *muzak.Context) (CurrentUser, error) {
    token, present := muzak.BearerToken(ctx)
    if !present {
        return CurrentUser{}, muzak.Unauthorized("unauthorized")
    }
    user, err := lookUp(token)
    if err != nil {
        return CurrentUser{}, muzak.Unauthorized("unauthorized")
    }
    return user, nil
}
r.Get("/items/{item_id}", func(ctx *muzak.Context, in ItemParams) (ItemOut, error) {
    user := muzak.From[CurrentUser](ctx)
    return ItemOut{ID: in.ID, Owner: user.Username}, nil
}, muzak.Needs(core.GetCurrentUser))

muzak.From[CurrentUser](ctx) is checked at compile time. There is no interface{}, no type assertion, no service locator and no string key to mistype. The type parameter of Needs is inferred from the provider, so it is never written at the call site either.

Resolved values live on the request's Context and are cleared when it returns to the pool, so two concurrent requests can never see each other's values.

From against TryFrom

From panics when the route never declared the type, because that is a bug in the wiring rather than a condition to handle. The panic is caught by the recovery middleware and reported as a 500 with the details logged, but it is a mistake to fix rather than an error to recover from.

Where the absence of a dependency is a legitimate state, use TryFrom.

// UserOrIPTracker spends a request from the caller's budget when there is a
// caller, and from the address's otherwise.
//
// The identity is read with TryFrom rather than From, because most routes here
// resolve no user at all and an absent dependency is a legitimate state for
// this tracker rather than a programming error.
func UserOrIPTracker(ctx *muzak.Context) (string, error) {
    if user, ok := muzak.TryFrom[CurrentUser](ctx); ok {
        return "user:" + user.Username, nil
    }
    return muzak.IPTracker(ctx)
}

Declaring a provider for a whole router

Needs is a shared option, so a router can declare it once for everything beneath it.

r := muzak.NewRouter(muzak.WithTags("items"), muzak.Needs(core.GetCurrentUser))

Declaring the same type more than once along the chain is allowed, and the most recent declaration wins, which lets a route override a dependency its router declared.

The order things run in

For every request:

  1. The middleware chain: request identifier, panic recovery, access log, security headers, then anything installed with App.Use.
  2. The rate limit count, unless the policy asked for it after the dependencies.
  3. Guards, outermost first.
  4. Providers, in declaration order.
  5. Binding, which fills the input type from the request.
  6. Validation, which runs the input model's Validate method.
  7. The handler.

Guards running before validation is deliberate: an unauthenticated caller gets 401, not a map of your schema.

Two consequences are worth knowing:

  • Every guard runs before any provider, whatever scope each was declared on. A guard cannot read a value a provider produced, including a value published with WithSingleton, because none of them has resolved yet. Give the guard what it needs by closing over it, which is what core.GetTokenHeader(settings) does above, or move the check into a provider of its own.
  • Binding happens after the dependencies, so a guard and a provider see the raw request through the context rather than the typed input.

Singletons

A singleton is a value shared by every request rather than resolved per request. There are two ways to publish one.

WithSingleton publishes a value that already exists, which is the usual case:

settings := muzak.MustLoadConfig[core.Settings](muzak.EnvFile(".env"))
store := core.NewItemStore()
models := core.NewModelRegistry()

app := muzak.New(muzak.AppOptions{Title: settings.AppName, Addr: settings.Addr},
    muzak.WithSingleton(settings),
    muzak.WithSingleton(models),
    muzak.WithSingleton(store, store.Lifecycle()),
)

Handlers read it by type, with no cast:

func ListItems(ctx *muzak.Context, _ muzak.Empty) (schemas.ItemListOut, error) {
    store := muzak.From[*core.ItemStore](ctx)
    settings := muzak.From[core.Settings](ctx)

    stored := store.List()
    items := make([]schemas.ItemOut, 0, len(stored))
    for _, item := range stored {
        items = append(items, schemas.ItemOut{ID: item.ID, Name: item.Name})
    }
    return schemas.ItemListOut{Items: items, Limit: settings.ItemsPerUser}, nil
}

Singleton is the lazy counterpart, for a value expensive enough that building it at start-up is not worth it:

muzak.Singleton(func(ctx *muzak.Context) (*template.Template, error) {
    return template.ParseGlob("templates/*.html")
})

The provider runs on the first request that needs the value, receiving that request's Context. It must not retain that context, read request-specific state from it, or return a value that is unsafe for concurrent use, because every later request shares the same value. An error from the provider is cached too, so a failing singleton fails every request rather than being retried.

Whichever form you use, the value is shared, so it must be safe for concurrent use.

Singletons and lifecycle

If a published value implements muzak.Lifecycle, or a LifecycleFunc option is supplied alongside it, the value is also registered as a lifecycle component: started before the server accepts traffic and stopped after the server has drained. That is what store.Lifecycle() is doing above, and it is covered in Lifecycle.

Errors from a dependency

A guard or a provider that returns an error abandons the request, and the error becomes the response exactly as one returned from a handler would. Returning muzak.Unauthorized("") is the idiomatic way to reject, and there is a constructor like it for every status worth naming; any other error type becomes an opaque 500 with the real cause logged. See Error Handling.

A worked example

Here is a WebSocket credential resolved before the handshake completes, which is what lets an unauthenticated peer receive a readable JSON error rather than a socket that closes a moment after it opened.

// SessionOrToken is the caller of a WebSocket route, resolved from either a
// session cookie or a query parameter.
//
// A browser cannot set headers on a WebSocket handshake, so the two places a
// credential can arrive are a cookie the browser attaches itself and a query
// parameter the page puts in the URL.
type SessionOrToken struct {
    Value      string
    FromCookie bool
}

func GetSessionOrToken(ctx *muzak.Context) (SessionOrToken, error) {
    if cookie, err := ctx.Cookie("session"); err == nil && cookie.Value != "" {
        return SessionOrToken{Value: cookie.Value, FromCookie: true}, nil
    }
    if token := ctx.Query("token"); token != "" {
        return SessionOrToken{Value: token}, nil
    }
    return SessionOrToken{}, muzak.Unauthorized("a session cookie or a token query parameter is required")
}

Where to go next

Lifecycle covers resources that must be opened before traffic and closed after it, and Error Handling covers the errors a dependency returns.

Open source under MIT / Apache-2.0 · sustained by the people who ship on it.Built on net/http, and nothing else.