{"record":{"id":"c4c9362bfa03a8a8","repo":"gastownhall/beads","slug":"httpapi-the-configured-provider-fires-this-worksp","errorCode":null,"errorMessage":"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)","messagePattern":"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\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/httpapi/server.go","lineNumber":672,"sourceCode":"\t\treturn errors.New(\"httpapi: no database source: set Provider, or \" + roleSourceNames + \" together\")\n\t// The conditional role, checked where every other configuration mistake is.\n\t// A workspace that HAS a journal and a server that cannot read it is the\n\t// one combination that would bind, answer every other route, and fail this\n\t// one — with a nil dereference, which is the shape checkDatabaseSource\n\t// exists to prevent. A workspace with the journal off needs no reader and\n\t// this says nothing about it.\n\tcase cfg.Provider == nil && cfg.EventsJournalEnabled && cfg.EventsJournal == nil:\n\t\treturn errors.New(\"httpapi: this workspace's events journal is enabled but no EventsJournal reader was configured; \" +\n\t\t\t\"take one off the store (storage.EventsJournalCursor), or serve a workspace with the journal off\")\n\tcase anyRoleFiresHooks(cfg):\n\t\treturn errors.New(\"httpapi: a configured role fires this workspace's hooks; \" +\n\t\t\t\"this server does not run hooks, so take the roles from the store beneath the hook decorator \" +\n\t\t\t\"((*storage.HookFiringStore).Unwrap)\")\n\tcase uow.ProviderFiresHooks(cfg.Provider):\n\t\t// The same refusal for the other database source. A provider's roles\n\t\t// carry whatever the provider carries, so a hook-firing one would run a\n\t\t// user's subprocess per served mutation just as a hook-firing role does.\n\t\treturn errors.New(\"httpapi: the configured provider fires this workspace's hooks; \" +\n\t\t\t\"this server does not run hooks, so pass the provider beneath the hook layer \" +\n\t\t\t\"(uow.UnwrapProvider)\")\n\t}\n\treturn nil\n}\n\n// Addr is the bound address, which is the only way to discover the port under\n// the ephemeral default.\nfunc (s *Server) Addr() string { return s.listener.Addr().String() }\n\n// Serve accepts requests until ctx is canceled, then drains. It returns nil\n// on a clean shutdown; a listener failure is returned as-is.\n//\n// The drain budget covers a committing request that is mid-retry, because\n// Shutdown does not cancel in-flight handler contexts: killing such a\n// connection early would leave the client unable to tell whether its write\n// landed.\nfunc (s *Server) Serve(ctx context.Context) error {","sourceCodeStart":654,"sourceCodeEnd":690,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/httpapi/server.go#L654-L690","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass the provider through uow.UnwrapProvider to get the provider beneath the hook layer before putting it in Config","Check uow.ProviderFiresHooks(provider) == false before constructing the server","If hooks must fire on served mutations, use a code path that declares and honors that contract instead of this server"],"exampleFix":"// before\nsrv, err := httpapi.New(httpapi.Config{Provider: hookFiringProvider})\n// after\nsrv, err := httpapi.New(httpapi.Config{Provider: uow.UnwrapProvider(hookFiringProvider)})","handlingStrategy":"validation","validationCode":"if uow.ProviderFiresHooks(cfg.Provider) {\n    return errors.New(\"provider fires hooks; wrap with uow.UnwrapProvider before serving\")\n}","typeGuard":null,"tryCatchPattern":"if err := httpapi.New(cfg); err != nil {\n    if strings.Contains(err.Error(), \"provider fires this workspace's hooks\") {\n        cfg.Provider = uow.UnwrapProvider(cfg.Provider)\n        return httpapi.New(cfg)\n    }\n    return err\n}","preventionTips":["Run uow.ProviderFiresHooks(provider) as an assert in tests for server wiring","Never reuse the CLI's hook-firing provider directly for the HTTP server","Keep provider construction and server construction in the same place to preserve the unwrap step"],"tags":["httpapi","hooks","uow","configuration"],"backgroundTag":"hook-firing-store-misconfigured","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}