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.
A provider's value is read with From or, declared on the input type, with a Dep field. A
provider that has to clean up after the request, such as a database transaction, is declared
with Acquire. Tests replace any provider with Override.
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.
// 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
}
A provider can resolve the caller from anything the request carries: a header, as here, or
the session a browser signed in with, read through ctx.Session() (see
Sessions).
The handler reads the value in one of two ways, and both are checked by the compiler. 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.
On the input, with Dep
A muzak.Dep[T] field on the input type declares what the handler depends on, and Get
reads it:
type ReadItem struct {
ID string `path:"item_id"`
User muzak.Dep[CurrentUser]
}
func ReadItem(ctx *muzak.Context, in schemas.ReadItem) (schemas.ItemOut, error) {
return schemas.ItemOut{ID: in.ID, Owner: in.User.Get().Username}, nil
}
r.Get("/items/{item_id}", handlers.ReadItem, muzak.Needs(core.GetCurrentUser))
What a Dep buys over From is when a mistake surfaces. Every Dep[T] must be satisfied
by a provider of exactly type T declared on the route or a router above it, with
Needs, Acquire, Transaction, Singleton or WithSingleton. A route that forgot one
is not built, and the build error names the route, the field and the type:
muzak: GET /items/{item_id}: field User is a muzak.Dep[core.CurrentUser], but no provider of
exactly core.CurrentUser is declared on the route or a router above it; declare one with
muzak.Needs, muzak.Acquire, muzak.Transaction, muzak.Singleton or muzak.WithSingleton
A provider of *CurrentUser does not satisfy a Dep[CurrentUser], and the error says so
when that is the mix-up.
The verified caller is the one value no provider is needed for. A route whose WithSecurity
names a JWTBearer or APIKeyVerifier scheme fills a Dep[*muzak.Claims] or a
Dep[*muzak.APIKeyPrincipal] from the credential it verified, and the build refuses such a
field unless every way into the route goes through that scheme. See
Authentication.
A Dep is never read from the request:
- It is not a JSON body member. A body that names it is refused as an unknown member, or
ignored under
AllowUnknownFields, so a client can never set or null the value. - It cannot carry a
path,query,header,cookie,formorfiletag; one that does is a build error. - It does not appear in the OpenAPI document, as a parameter or as a body member.
It is filled after the dependencies have resolved and before the input is bound and
validated, so a Validate method may read it. Muzak also calls Validate once at build time
on a zero input, to learn its rules, and there Get returns the zero value, so code that
reads a Dep in Validate must cope with it.
A Dep must be a named, exported field at the top level of the input, or of a struct
embedded in it by value, which is how a set of dependencies is shared between inputs:
type Authenticated struct {
User muzak.Dep[CurrentUser]
}
type UpdateItem struct {
Authenticated
ID string `path:"item_id"`
Title string `json:"title"`
}
A Dep behind a pointer, inside a slice, an array or a map, in a nested struct, embedded
directly, or unexported would never be filled, so each of those is a build error rather
than an empty value at run time.
A Dep is filled in place and costs no allocation, and an input without one pays nothing
for the feature. Dep works on event stream and WebSocket routes too.
In code, with From
muzak.From[CurrentUser](ctx) returns the same value. It remains the way to read a
dependency where there is no input type: in a guard, a provider, or a helper shared between
handlers.
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))
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. A Dep catches the same mistake when the application is built instead.
A provider of an interface type may return nil, nil, which is the natural encoding of an
anonymous caller. From, TryFrom and Dep.Get return the nil the provider produced, rather
than failing the request.
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)
}
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.
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 overrides the value, not the check. Every provider on the chain runs,
outermost first, and the first error aborts the request; From, TryFrom and Dep then
return the value of the innermost declaration. That matters because a provider is often also
an authorization step: a router included with Needs(RequireAdmin) keeps rejecting
non-admins even when a route inside it declares Needs(CurrentUser) of the same type, rather
than the nearer declaration silently removing the outer one and the check with it. A provider
that calls From for its own type sees the value produced by the nearest enclosing
declaration of that type, since its own value does not exist yet, which lets an inner
provider refine an outer one instead of repeating its work.
If an outer provider was being deliberately replaced, it now also runs and can fail the request. Declare it only where it applies, or give the two values distinct types.
Dependencies that clean up: Acquire
Some values have to be released when the request is over: a transaction committed or rolled
back, a connection returned to its pool, a lock released, a temporary file removed. FastAPI
writes these as a dependency that yields; Muzak writes them with Acquire, whose provider
returns a Release beside the value:
// OpenSession checks a session out of the pool for the request.
func OpenSession(ctx *muzak.Context) (*Session, muzak.Release, error) {
s, err := pool.Open(ctx.Context())
if err != nil {
return nil, nil, err
}
return s, func(failure error) error {
return s.Close()
}, nil
}
r.Post("/orders", handlers.PlaceOrder, muzak.Acquire(core.OpenSession))
The value is read like any other, with a Dep[*Session] field or From, and the provider
runs where a Needs provider would. A Release is func(failure error) error. failure is
nil when the request succeeded and otherwise says why it did not, which is what makes "keep
the work only if everything succeeded" a single if.
The contract
- Exactly once. A release runs once for every provider call that returned a nil error,
and never for one that returned an error. A provider that fails part way through undoes
what it had acquired itself; a
Releasereturned alongside an error is not called. A nilReleasemeans there is nothing to release. - Last acquired, first released. Releases run in the reverse of the order their values were acquired, across the whole chain: the application's, then the router's, then the route's are acquired in that order and released the other way round.
- On every path. The table below lists what each release is handed.
- Before the response is written. For an ordinary route, releases run after the handler has returned and its result has been encoded, and before a byte of the response is written. A release that fails can therefore still turn the success into an error.
- No leaks between requests. Releases are held on the request's pooled
Contextand cleared with it, so a request can never run a release another request acquired.
| How the request ended | failure handed to each release |
|---|---|
| The handler succeeded and the response encoded | nil |
| The handler returned an error | That error |
| The handler, a guard or a provider panicked | An error describing the panic; the panic is still the usual 500, logged once |
A later provider, or a later Acquire, refused | Its error |
| The rate limit counted after the dependencies refused | The 429 |
| Binding or validation failed | The *muzak.ValidationError (a 422) |
| The response could not be encoded | The encoding error (a 500) |
| The client went away and the handler returned the context's error | That error, such as context.Canceled |
| An event stream or a WebSocket handler returned | Whatever it returned |
| An earlier release (acquired later) failed | That release's error |
A guard runs before every provider, so a guard that refuses acquires nothing and has nothing
to release. An Acquire given a nil provider is a build error.
Tasks registered with ctx.AfterResponse follow the same verdict. They run only when the
handler returned nil, and on a route with an Acquire provider they are also dropped when
the value fails to encode, because the releases were told the request failed and a
Transaction has been rolled back. See Background Tasks.
When a release fails
The error a Release returns matters only when failure is nil.
- The response has not started. This is every buffered response, JSON, HTML or
204 No Content. The release error replaces the success and is rendered exactly as a handler's error would be: amuzak.HTTPErrorchooses its own status (a release may returnmuzak.Conflict(...)for a serialization failure), and any other error becomes an opaque 500 with the cause logged and never sent. Headers the handler set for the success, such as anETag, are dropped as they are for any error. - The response has started. The handler wrote to
ctx.ResponseWriter(), streamed events, hijacked or upgraded the connection. The status is gone, so the error is treated the way the handler returning it at that point would be: it is logged, a response the handler started is aborted rather than completed, an event stream logs it as the stream's failure, and a WebSocket closes with1011rather than1000. - The releases still to run are handed the failed release's error, so a second transaction is rolled back when the first one's commit failed.
When failure is not nil the response is already the failure's, so a release error is
logged against its dependency type and changes nothing. A Release that panics is recovered,
logged with its stack, and counted as having returned an error; the remaining releases still
run.
A Release receives no Context. It runs on the request's goroutine before the Context
returns to the pool, so it may use what the provider captured, but must not keep the
Context.
Event streams and WebSockets
On an SSE or WebSocket route the releases run as soon as the handler returns, which is the
end of the stream or the connection, and are handed what the handler returned. A client
that leaves usually makes the handler return an error (muzak.ErrSSEStreamEnded, a
*muzak.WSCloseError or the context's error), so a value acquired for a stream is released
as failed unless the handler returns nil. A Transaction spanning a stream or a connection
therefore commits only when the handler returns nil: return nil to keep the work.
Mounts and the documentation
A file mount, a handler served with Router.Mount and the documentation run the providers
they inherit, for their verdict, so an Acquire declared on the whole application acquires
and releases for every request they answer. None of them has a handler error to report:
- A file mount's releases are handed nil once the file is served, and the refusal otherwise, including when the file mount answers beneath a handler mount.
- A mounted handler's are handed nil once it returns without panicking, whatever status it wrote, and an error describing the panic otherwise. A release that fails after the handler has written aborts the response.
- The documentation's are handed nil once the page or the document is served, and the refusal otherwise.
Declare Acquire on the routers that need it, not on New.
Not for singletons
Acquire is request-scoped by construction. A value shared by every request has no request
to be released at the end of, so there is no singleton that releases: a resource that lives
as long as the application is published with WithSingleton and closed through
Lifecycle.
Transactions
muzak.Transaction is the Acquire most applications need: a *sql.Tx begun for the
request and ended with it.
type PlaceOrder struct {
Tx muzak.Dep[*sql.Tx]
Item string `json:"item"`
}
The transaction is begun with the request's context and the given *sql.TxOptions, which
may be nil. It is committed when the handler, and everything after it, succeeded: the
handler returned no error, the response encoded, and no release acquired after it failed.
It is rolled back otherwise, including when the handler panicked, a later provider or the
rate limit refused the request, binding or validation failed, or the client went away.
| What happened | Database sees | Client sees |
|---|---|---|
| Success | COMMIT | The handler's response |
| Handler error, panic, 422, later refusal | ROLLBACK | That error |
| A guard refused | Nothing: no transaction was begun | The guard's error |
BeginTx failed | Nothing | An opaque 500, cause logged |
COMMIT failed | The failed commit | An opaque 500, cause logged, never the driver's text |
| The client went away | ROLLBACK, by database/sql when the context ended | Nothing; it left |
The route's Timeout passed, even if the handler then succeeded | ROLLBACK, by database/sql when the deadline passed | 503 with Retry-After: 1 |
A commit that fails turns the response into a 500, because the client must not be told that
work it asked for was done when it was not. A rollback that finds the transaction already
ended, which database/sql does itself when the request's context is cancelled, is not an
error. Even a handler that ignores the cancellation and returns success is not committed:
database/sql has already rolled the transaction back once the context ended. When it ended
because the route's Timeout passed, that success is answered with
the 503 the deadline calls for.
Declare Transaction where the writes are, on the routers or routes that need it. Declared
on New, it would also begin a transaction for every file a mount serves. A Transaction
given a nil *sql.DB is a build error.
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, through From or a Dep field:
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.
Only a successful value is cached. An error, or a panic, fails the request that triggered it and the requests that waited on the same attempt, and the next request runs the provider again. Caching the failure meant a provider that panicked once, or that failed because the first client disconnected and cancelled its request, failed every later request until restart, and one aborted request could trigger it. One attempt is in flight at a time, so a slow failure does not multiply into queued latency: waiting requests share its outcome and stop waiting when their own request ends.
While it runs, ctx.Context() is a context that keeps the triggering request's values but not
its cancellation or its deadline, because the value outlives that request. A provider that
relied on the request deadline to bound its I/O should apply its own:
muzak.Singleton(func(ctx *muzak.Context) (*Pool, error) {
c, cancel := context.WithTimeout(ctx.Context(), 5*time.Second)
defer cancel()
return dial(c)
})
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.
Overriding dependencies in tests
A test swaps a real provider for a fake with App.Override, without touching the routes.
With the test client it is an option:
func TestReadItemAsAlice(t *testing.T) {
client := testclient.New(t, buildApp(),
testclient.Override(func(*muzak.Context) (core.CurrentUser, error) {
return core.CurrentUser{Username: "alice"}, nil
}))
client.Get("/items/foo").AssertJSON(`{"id":"foo","owner":"alice"}`)
}
Without it, call the method on the application before it is built:
app := buildApp()
app.Override(func(*muzak.Context) (core.CurrentUser, error) {
return core.CurrentUser{Username: "alice"}, nil
})
| Rule | Why |
|---|---|
Every provider of exactly type T is replaced, on every route, file mount, handler given to Router.Mount and the documentation | One line covers the whole application, however many routers declare the provider |
From, TryFrom and Dep all receive the override's value | The handler cannot tell, which is the point |
| Guards are left alone | A test still passes or fails them the way a client would |
| The value is replaced, the lifetime kept | A request-scoped provider's override runs per request; a singleton's runs once, lazily; Cache-Control does not change |
| An override replaces but never adds a provider | A route that forgot to declare one fails to build under test exactly as it does in production |
| An override for a type nothing provides is a build error | Catches overriding CurrentUser where the application provides *CurrentUser |
Overrides belong to one App | Two applications in one test binary never see each other's, even when they share a provider option |
| The last override of a type wins | A helper can set a default and a test refine it |
Override after the application is built panics | It could no longer apply, and it would race with the requests reading the chains |
| An override given a nil function is a build error | It could only ever fail the requests it reached |
The overridden provider never runs, and neither does its Release if it was an Acquire.
A fake that needs a release of its own uses App.OverrideAcquire, or
testclient.OverrideAcquire, with the same contract as Acquire. It cannot replace a
singleton, which is a build error: a fake released after every request would be tested under
a lifetime production never has.
An override may return an error too, which is how a test exercises the refusal path:
testclient.Override(func(*muzak.Context) (core.CurrentUser, error) {
return core.CurrentUser{}, muzak.Forbidden("suspended")
})
The order things run in
For every request:
- The middleware chain: request identifier, tracing and the request observer when they are
configured, security headers, panic recovery, the health probes (answered there and going
no further), locale and the access log, then the allowed-hosts check and the HTTPS
redirect, the CORS policy and cross-origin protection when each is configured, then
anything installed with
App.Use. See Middleware. - The rate limit count, unless the policy asked for it after the dependencies.
- The route's Timeout starts, when it has one.
- The verifying security schemes the route's
WithSecuritynames, such asJWTBearer. - The body a
CaptureBodyroute keeps is read. - Guards, outermost first.
- Providers,
AcquireandTransactionincluded, in declaration order. - The rate limit count, if the policy asked for it after the dependencies.
Depfields are filled.- Binding, which fills the rest of the input type from the request.
- Validation, which runs the input model's
Validatemethod. - The handler.
- The response is encoded.
- Releases, last acquired first, then the session cookie when there is one to write.
- The response is written.
- Tasks registered with
ctx.AfterResponseare handed to the background workers.
Guards running before validation is deliberate: an unauthenticated caller gets 401, not
a map of your schema.
Three 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 whatcore.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.
- Releases run before the response is written, so what a release does (a commit, say) has happened by the time the client reads the answer, and a release that fails changes it.
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. The same holds for a Release that fails after a success. See
Error Handling.
A worked example
Here is the caller of a WebSocket route 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.
// Caller is who is on the other end of a WebSocket route.
type Caller struct {
Username string
FromToken bool
}
// GetCaller resolves the caller from the session a sign-in stored, or from a
// token query parameter.
//
// A browser cannot set headers on a WebSocket handshake, so the two places a
// credential can arrive are the session cookie the browser attaches itself and
// a query parameter the page puts in the URL.
func GetCaller(ctx *muzak.Context) (Caller, error) {
if user, ok := muzak.SessionGet[string](ctx.Session(), "user"); ok {
return Caller{Username: user}, nil
}
if token := ctx.Query("token"); token != "" {
user, err := tokens.Verify(ctx.Context(), token)
if err != nil {
return Caller{}, muzak.Unauthorized("the token is not valid")
}
return Caller{Username: user, FromToken: true}, nil
}
return Caller{}, muzak.Unauthorized("sign in, or pass a token query parameter")
}
ctx.Session() needs AppOptions.Sessions, and reads the session the first time it is
asked, here during the handshake. Reading is all a WebSocket route should do with it: the
connection's response has started by the time the request ends, so a change is not saved
then, and a warning is logged instead. Because the session cookie travels with any
request the browser makes, the handshake's Origin is what keeps another site's page from
opening the socket as the visitor; WSOptions.AllowedOrigins admits the server's own origin
and the ones it lists. See Sessions and
WebSockets.
Where to go next
Lifecycle covers resources that must be opened before traffic and closed after it, Error Handling covers the errors a dependency returns, Timeouts covers the deadline the dependencies and the handler share, and Testing covers the test client the overrides above plug into.