gastownhall/beads · error

httpapi: the configured provider fires this workspace's hook

Error message

httpapi: the configured provider fires this workspace's hooks; this server does not run hooks, so pass the provider beneath the hook layer (uow.UnwrapProvider)

What it means

This startup validation error means the configured uow.Provider would fire the workspace's hooks on each served mutation. The HTTP server contractually does not run hooks, so a hook-firing provider is refused exactly like a hook-firing role would be — otherwise a user's hook subprocess would run per served mutation. The fix is to wrap the provider beneath the hook layer via uow.UnwrapProvider.

Source

Thrown at internal/httpapi/server.go:672

		return errors.New("httpapi: no database source: set Provider, or " + roleSourceNames + " together")
	// The conditional role, checked where every other configuration mistake is.
	// A workspace that HAS a journal and a server that cannot read it is the
	// one combination that would bind, answer every other route, and fail this
	// one — with a nil dereference, which is the shape checkDatabaseSource
	// exists to prevent. A workspace with the journal off needs no reader and
	// this says nothing about it.
	case cfg.Provider == nil && cfg.EventsJournalEnabled && cfg.EventsJournal == nil:
		return errors.New("httpapi: this workspace's events journal is enabled but no EventsJournal reader was configured; " +
			"take one off the store (storage.EventsJournalCursor), or serve a workspace with the journal off")
	case anyRoleFiresHooks(cfg):
		return errors.New("httpapi: a configured role fires this workspace's hooks; " +
			"this server does not run hooks, so take the roles from the store beneath the hook decorator " +
			"((*storage.HookFiringStore).Unwrap)")
	case uow.ProviderFiresHooks(cfg.Provider):
		// The same refusal for the other database source. A provider's roles
		// carry whatever the provider carries, so a hook-firing one would run a
		// user's subprocess per served mutation just as a hook-firing role does.
		return errors.New("httpapi: the configured provider fires this workspace's hooks; " +
			"this server does not run hooks, so pass the provider beneath the hook layer " +
			"(uow.UnwrapProvider)")
	}
	return nil
}

// Addr is the bound address, which is the only way to discover the port under
// the ephemeral default.
func (s *Server) Addr() string { return s.listener.Addr().String() }

// Serve accepts requests until ctx is canceled, then drains. It returns nil
// on a clean shutdown; a listener failure is returned as-is.
//
// The drain budget covers a committing request that is mid-retry, because
// Shutdown does not cancel in-flight handler contexts: killing such a
// connection early would leave the client unable to tell whether its write
// landed.
func (s *Server) Serve(ctx context.Context) error {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pass the provider through uow.UnwrapProvider to get the provider beneath the hook layer before putting it in Config
  2. Check uow.ProviderFiresHooks(provider) == false before constructing the server
  3. If hooks must fire on served mutations, use a code path that declares and honors that contract instead of this server

Example fix

// before
srv, err := httpapi.New(httpapi.Config{Provider: hookFiringProvider})
// after
srv, err := httpapi.New(httpapi.Config{Provider: uow.UnwrapProvider(hookFiringProvider)})
Defensive patterns

Strategy: validation

Validate before calling

if uow.ProviderFiresHooks(cfg.Provider) {
    return errors.New("provider fires hooks; wrap with uow.UnwrapProvider before serving")
}

Try / catch

if err := httpapi.New(cfg); err != nil {
    if strings.Contains(err.Error(), "provider fires this workspace's hooks") {
        cfg.Provider = uow.UnwrapProvider(cfg.Provider)
        return httpapi.New(cfg)
    }
    return err
}

Prevention

When it happens

Trigger: Constructing the server with a Config whose Provider satisfies uow.ProviderFiresHooks — i.e. a provider that carries hook-firing roles. checkDatabaseSource hits this case only when no role fires hooks (the earlier case) but the provider itself does.

Common situations: Passing a provider built over a hook-wrapped store directly into httpapi.Config, typically after enabling on_update hooks; also arises when reusing a provider meant for the CLI, where hooks firing is desired.

Related errors


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