gastownhall/beads · error

httpapi: this server has no unit-of-work provider; it answer

Error message

httpapi: this server has no unit-of-work provider; it answers from configured issue roles

What it means

WithUOW opens a unit of work around fn, but a server configured with issue roles instead of a Provider has no unit of work to open — the roles own their own transactions. Instead of dereferencing a nil provider, WithUOW returns this error stating the server is roles-backed.

Source

Thrown at internal/httpapi/server.go:1228

}

// WithUOW runs fn inside one unit of work and guarantees the rollback.
//
// The close context is DETACHED on purpose. Close sends ROLLBACK on the pinned
// connection, and the transaction layer POISONS that connection if the send
// fails (internal/storage/uow/doltserver_tx.go) — go-sql-driver's session reset
// does not clear an open transaction, so a session that may still be in one
// must never go back to the pool. Correctness is therefore safe either way, but
// closing with the request's own canceled context would fail the ROLLBACK
// immediately and burn one pinned session on every client disconnect. Reads
// never commit.
//
// It is provider-only, and says so rather than dereferencing nil: a
// roles-backed server has no unit of work to open, and the roles it does hold
// own their own transactions.
func (s *Server) WithUOW(ctx context.Context, rec *reqInfo, fn func(uow.UnitOfWork) error) error {
	if s.provider == nil {
		return errors.New("httpapi: this server has no unit-of-work provider; it answers from configured issue roles")
	}
	start := time.Now()
	uw, err := s.provider.NewUOW(ctx)
	if rec != nil {
		rec.uowWait = time.Since(start)
	}
	if err != nil {
		return err
	}
	defer func() {
		closeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), uowCloseTimeout)
		defer cancel()
		uw.Close(closeCtx)
	}()
	return fn(uw)
}

// acquire takes a database slot, or gives up. A timed-out wait is ErrBusy, not

View on GitHub (pinned to 71377f2769)

Solutions

  1. Configure the server with a Provider if you need unit-of-work semantics in handlers
  2. Branch on s.provider == nil (or your own config knowledge) and take the roles-backed code path instead of calling WithUOW
  3. Refactor the handler to use the role-specific APIs, which manage their own transactions

Example fix

// before
err := srv.WithUOW(ctx, rec, func(uw uow.UnitOfWork) error { return mutate(uw) })
// after
if srv.Provider() == nil {
    return mutateWithRoles(srv, ctx)
}
return srv.WithUOW(ctx, rec, func(uw uow.UnitOfWork) error { return mutate(uw) })
Defensive patterns

Strategy: type-guard

Validate before calling

if srv.Provider() == nil {
    return errors.New("roles-backed server: use role APIs, not WithUOW")
}

Type guard

func hasProvider(s *httpapi.Server) bool { return s.Provider() != nil }

Try / catch

err := srv.WithUOW(ctx, rec, fn)
if err != nil && strings.Contains(err.Error(), "no unit-of-work provider") {
    return fallbackToRolesPath(ctx, srv)
}

Prevention

When it happens

Trigger: Calling s.WithUOV/WithUOW (Server.WithUOW) on a server built with Config without Provider set (roles-backed server). Any request handler or middleware that assumes provider-backed configuration and calls WithUOW will get this error.

Common situations: Mixing configuration modes: deployment switched from provider-backed to roles-backed (or vice versa) but a handler still calls WithUOW; embedding the httpapi server in another binary and calling WithUOW unconditionally.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/bbe111a4205f3482. Report an issue: GitHub.