Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ServerContext

*maniflex.ServerContext is the object threaded through every pipeline step for one HTTP request. Steps read from it, write to it, and call next() to proceed. Middleware does the same. This page documents the fields and methods middleware will commonly touch.

Lifecycle

A new ServerContext is constructed by the handler for every request, populated incrementally by the pipeline, and discarded once the response is written. It is not safe to share across requests or goroutines.

The fields populated by each step are:

StepSets
handler (before Auth)Request, Writer, Ctx, Model, Operation, ResourceID, RequestID, TraceID
AuthAuth (when user middleware populates it)
DeserializeRawBody, ParsedBody, Query, Files
Service(whatever user middleware sets)
DBDBResult, possibly Tx
ResponseResponse, then writes it to Writer

Routing context

Set by the handler before Auth runs; safe to read in any step.

FieldMeaning
Requestthe original *http.Request
Writerthe underlying http.ResponseWriter
Ctxthe request context.Context; cancellation propagates from here
Modelthe *ModelMeta for the resource — name, table, fields, relations
Operationthe Operation being performed (OpCreate, OpList, …)
ResourceIDthe {id} path parameter, empty for list and create
RequestIDchi’s request ID, echoed in X-Request-Id
TraceIDthe W3C traceparent header, when present

Step outputs

Populated in order by the pipeline.

FieldPopulated byType
RawBodyDeserialize[]byte — the raw request bytes
ParsedBodyDeserialize*RequestBody — read-only JSON-keyed body (mutate via SetField)
RecordDeserializethe typed record carrier (*T for ctx.Model) bound from the body
QueryDeserialize*QueryParams — pagination, filters, sorts, includes
FilesDeserialize (multipart only)map[string]*UploadedFile
DBResultDB*ListResult for lists; the record otherwise (a typed *T on reads)
ResponseResponse*APIResponse — the envelope written to the wire

Setting Response from any step causes the remaining steps to skip and the prepared envelope to be written. See Abort below.

Auth

Auth *AuthInfo is populated by Auth middleware. When nil, the request is anonymous.

type AuthInfo struct {
    UserID       string
    Roles        []string
    Claims       map[string]any
    TenantID     string
    IdentityType AuthIdentityType  // human, service_account, anonymous
    Scopes       []string
    SessionID    string
    AuthMethod   string             // "jwt", "api_key", "session", …
}

ctx.HasRole(role string) bool is a convenience wrapper that returns false when Auth is nil.

Transactions

Tx Tx carries the active transaction, if any. When set, the default DB step routes through it. ctx.BeginTx(ctx.Ctx, opts) returns a Tx and is the standard way for middleware to start one. See Transactions for the full pattern.

Aborting the pipeline

ctx.Abort(status int, code, message string) populates ctx.Response with an error envelope. The current middleware must then return nil without calling next(). Subsequent steps are skipped; the Response step writes the prepared error.

if header == "" {
    ctx.Abort(http.StatusUnauthorized, "UNAUTHORIZED", "missing token")
    return nil
}

Calling next() after Abort

Abort does not stop the pipeline — it only populates ctx.Response. If the middleware calls next() afterwards, the chain continues exactly as if the abort had not happened:

  • The remaining steps still execute, with all their side effects. The DB step will still issue its query and possibly modify the database; a Service middleware will still call out to external services.
  • Any of those steps may overwrite ctx.Response — for example, the DB step replaces it with a 404 NOT_FOUND if the record is missing, or the default Response step builds a 200 OK envelope from ctx.DBResult. Whichever step writes last wins.
  • If nothing downstream touches ctx.Response, the original abort envelope is preserved and sent to the client — but the side effects have already happened.

The result is almost always a bug: either the client sees a misleading status (a write succeeded but the response claims it was rejected), or the database is mutated by a request that was meant to be refused. Always return without next() after Abort.

Reading input

Three helpers wrap common request reads:

MethodPurpose
BindJSON(v any) errordecode the body into v, enforcing the 4 MB limit
URLParam(name string) stringread a chi URL parameter
QueryParam(name string) stringread a URL query parameter

BindJSON calls Abort internally on error and returns a non-nil error so the caller can return nil immediately.

The request body

ctx.ParsedBody holds the deserialized JSON (or multipart form) body as a *RequestBody. It is read-only: there is no exported way to index or assign it, so a stray ctx.ParsedBody["x"] = y is a compile error. This is deliberate. The body is mirrored onto a typed record (ctx.Record), and the only mutators — ctx.SetField / ctx.DeleteField — keep both in sync; writing the map directly would update one and not the other, and the change could be silently dropped at the DB step.

Reading

CallReturns
ctx.Field(name string) (any, bool)one field by its JSON name
ctx.ParsedBody.Has(name) boolwhether a key is present (an explicit null counts)
ctx.ParsedBody.Keys() []string / .Len() intthe top-level key set
ctx.ParsedBody.Map() map[string]anya copy of the body, for read-only consumers

All are nil-safe: ctx.ParsedBody is nil for body-less requests (GET, DELETE) and the readers return zero values rather than panicking.

For typed access, read the whole body as the concrete model struct:

u, ok := maniflex.For[User](ctx)   // (*User, bool) — false if no User body is bound
u, err := maniflex.Bind[User](ctx) // (*User, error) — errors when absent

// or adapt a typed handler straight into middleware:
server.Pipeline.Service.Register(
    maniflex.Handle(func(ctx *maniflex.ServerContext, u *User) error {
        if u.Age < 18 {
            ctx.Abort(http.StatusUnprocessableEntity, "TOO_YOUNG", "must be 18+")
        }
        return nil
    }),
    maniflex.ForModel("User"), maniflex.ForOperation(maniflex.OpCreate),
)

Writing

Middleware that injects or rewrites a field must go through these setters so the value reaches both the body and the typed record (and so the DB step persists it):

CallEffect
ctx.SetField(name string, value any)set a field by its JSON name
ctx.DeleteField(name string)remove a field (e.g. strip an input-only field)
server.Pipeline.Service.Register(func(ctx *maniflex.ServerContext, next func() error) error {
    ctx.SetField("owner_id", ctx.Auth.UserID) // force the owner server-side
    return next()
}, maniflex.ForOperation(maniflex.OpCreate))

Cross-step storage

For state that one middleware needs to pass to another:

ctx.Set("invoiceID", inv.ID)
// later, in another middleware:
id, ok := ctx.Get("invoiceID")

The store is per-request and discarded with the context.

Direct database access

Middleware that needs to reach beyond ctx.Model — to read another model, run a raw query, or take a row lock — has four entry points, all routed through ctx.Tx when one is active:

MethodPurpose
GetModel(name string) *ModelAccessorCRUD on any registered model (.List / .Read / .Create / .Update / .Delete)
RawQuery(sql string, args ...any) ([]map[string]any, error)parameterised SELECT
RawExec(sql string, args ...any) (int64, error)parameterised non-SELECT
LockForUpdate(modelName, id string) (map[string]any, error)pessimistic row lock; requires ctx.Tx

GetModel returns an accessor whose methods route through ctx.Tx when set, so middleware in a transaction does not have to thread the Tx manually.

Typed cross-model helpers

GetModel(name) is dynamic — string-named, exchanging map[string]any. For compile-time types use the generic free functions, which resolve the model from the type parameter and route through ctx.Tx the same way (so they also participate in maniflex.Batch):

u, err   := maniflex.Read[User](ctx, id)        // *User
all, err := maniflex.List[User](ctx, nil)        // []*User
created, err := maniflex.Create(ctx, &User{Name: "Jane"})
maniflex.Update(ctx, id, &User{ /* full record */ })
maniflex.Delete[User](ctx, id)

Results that are not a registered model — raw SQL, aggregates, recursive queries — use maniflex.Row (an alias for map[string]any); RawQuery, Aggregate, and RecursiveQuery return []maniflex.Row.

Logging

ctx.Logger() *slog.Logger returns a slog logger pre-seeded with request_id, trace_id, and service attributes, so log lines emitted from middleware are correlated automatically.

ctx.Logger().Info("payment captured",
    slog.String("invoice_id", inv.ID),
    slog.Float64("amount", inv.Total),
)

Service name

ctx.ServiceName() returns the Config.ServiceName configured on the server. Middleware uses this to enrich audit records or outgoing requests without holding a reference to the framework Config.

Next