You are reading the 0.3.0 documentation. The current release is 0.3.1. Read this page for it →

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, form or file tag; 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 Release returned alongside an error is not called. A nil Release means 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 Context and cleared with it, so a request can never run a release another request acquired.
How the request endedfailure handed to each release
The handler succeeded and the response encodednil
The handler returned an errorThat error
The handler, a guard or a provider panickedAn error describing the panic; the panic is still the usual 500, logged once
A later provider, or a later Acquire, refusedIts error
The rate limit counted after the dependencies refusedThe 429
Binding or validation failedThe *muzak.ValidationError (a 422)
The response could not be encodedThe encoding error (a 500)
The client went away and the handler returned the context's errorThat error, such as context.Canceled
An event stream or a WebSocket handler returnedWhatever it returned
An earlier release (acquired later) failedThat 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: a muzak.HTTPError chooses its own status (a release may return muzak.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 an ETag, 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 with 1011 rather than 1000.
  • 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 happenedDatabase seesClient sees
SuccessCOMMITThe handler's response
Handler error, panic, 422, later refusalROLLBACKThat error
A guard refusedNothing: no transaction was begunThe guard's error
BeginTx failedNothingAn opaque 500, cause logged
COMMIT failedThe failed commitAn opaque 500, cause logged, never the driver's text
The client went awayROLLBACK, by database/sql when the context endedNothing; it left
The route's Timeout passed, even if the handler then succeededROLLBACK, by database/sql when the deadline passed503 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
})
RuleWhy
Every provider of exactly type T is replaced, on every route, file mount, handler given to Router.Mount and the documentationOne line covers the whole application, however many routers declare the provider
From, TryFrom and Dep all receive the override's valueThe handler cannot tell, which is the point
Guards are left aloneA test still passes or fails them the way a client would
The value is replaced, the lifetime keptA 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 providerA 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 errorCatches overriding CurrentUser where the application provides *CurrentUser
Overrides belong to one AppTwo applications in one test binary never see each other's, even when they share a provider option
The last override of a type winsA helper can set a default and a test refine it
Override after the application is built panicsIt could no longer apply, and it would race with the requests reading the chains
An override given a nil function is a build errorIt 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:

  1. 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.
  2. The rate limit count, unless the policy asked for it after the dependencies.
  3. The route's Timeout starts, when it has one.
  4. The verifying security schemes the route's WithSecurity names, such as JWTBearer.
  5. The body a CaptureBody route keeps is read.
  6. Guards, outermost first.
  7. Providers, Acquire and Transaction included, in declaration order.
  8. The rate limit count, if the policy asked for it after the dependencies.
  9. Dep fields are filled.
  10. Binding, which fills the rest of the input type from the request.
  11. Validation, which runs the input model's Validate method.
  12. The handler.
  13. The response is encoded.
  14. Releases, last acquired first, then the session cookie when there is one to write.
  15. The response is written.
  16. Tasks registered with ctx.AfterResponse are 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 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.
  • 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.

Open source under MIT / Apache-2.0.Built on net/http, and nothing else.