{"record":{"id":"b771f74759f127fb","repo":"sipeed/picoclaw","slug":"channel-q-failed-to-decode-settings-w","errorCode":null,"errorMessage":"channel %q: failed to decode settings: %w","messagePattern":"channel %q: failed to decode settings: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/registry.go","lineNumber":51,"sourceCode":"//\n//\tfunc init() {\n//\t    channels.RegisterSafeFactory(config.ChannelTelegram,\n//\t        func(bc *config.Channel, c *config.TelegramSettings, b *bus.MessageBus) (channels.Channel, error) {\n//\t            return NewTelegramChannel(bc, c, b)\n//\t        })\n//\t}\nfunc RegisterSafeFactory[S any](\n\tchannelType string,\n\tctor func(bc *config.Channel, settings *S, bus *bus.MessageBus) (Channel, error),\n) {\n\tRegisterFactory(channelType, func(channelName, _ string, cfg *config.Config, b *bus.MessageBus) (Channel, error) {\n\t\tbc := cfg.Channels[channelName]\n\t\tif bc == nil {\n\t\t\treturn nil, fmt.Errorf(\"channel %q: config not found\", channelName)\n\t\t}\n\t\tdecoded, err := bc.GetDecoded()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"channel %q: failed to decode settings: %w\", channelName, err)\n\t\t}\n\t\tsettings, ok := decoded.(*S)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"channel %q: expected %T settings, got %T\", channelName, (*S)(nil), decoded)\n\t\t}\n\t\treturn ctor(bc, settings, b)\n\t})\n}\n\n// getFactory looks up a channel factory by name.\nfunc getFactory(name string) (ChannelFactory, bool) {\n\tfactoriesMu.RLock()\n\tdefer factoriesMu.RUnlock()\n\tf, ok := factories[name]\n\treturn f, ok\n}\n\n// GetRegisteredFactoryNames returns a slice of all registered channel factory names.","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/registry.go#L33-L69","documentation":"Inside the RegisterSafeFactory wrapper, bc.GetDecoded() parses/decrypts a channel's settings block into a concrete settings struct; failure is wrapped as \"channel %q: failed to decode settings\". The underlying cause is preserved with %w and is typically malformed settings content, a wrong shape for the target struct, or a secret that cannot be decrypted with the current key.","triggerScenarios":"Settings YAML/JSON for the channel is syntactically broken or has wrong field types (e.g. webhooks: \"string\" where a map is expected); settings stored encrypted but the decryption key changed or is missing; env-var placeholders in settings that expand to invalid values; schema drift after an upgrade moved fields.","commonSituations":"Upgrading the app to a version with a new settings schema while old config files keep removed fields with wrong types; moving an encrypted config file between hosts with different secret keys; hand-editing a locked/encrypted channel block; secrets referenced by name that no longer resolve.","solutions":["Unwrap the error (errors.Unwrap / %v) — decode libraries name the exact field and reason; fix that field in the channels.<name> stanza.","If settings are encrypted/secret-protected, ensure the decryption key/environment (devkey/secret registry) is the one that encrypted them, or re-enter the settings in plaintext and re-encrypt.","Run a config-validate pass or load the config in a test harness to catch decode errors before channel startup.","After upgrades, diff your settings block against the current struct definition for that channel type."],"exampleFix":"// before: shipping config errors to users\nch, err := channels.Build(name, cfg, bus)\nif err != nil { return err }\n\n// after: log the decode cause with the channel name\nif err != nil {\n    var decodeErr *json.UnmarshalTypeError // or yaml error via errors.As\n    if errors.As(err, &decodeErr) {\n        log.Printf(\"config error in channels.%s field %s: %v\", name, decodeErr.Field, err)\n    }\n    return err\n}","handlingStrategy":"validation","validationCode":"// dry-run decode during config validation\nfor name, bc := range cfg.Channels {\n    if _, err := bc.GetDecoded(); err != nil {\n        return fmt.Errorf(\"channel %s settings invalid: %w\", name, err)\n    }\n}","typeGuard":null,"tryCatchPattern":"// if err != nil && strings.Contains(err.Error(), \"failed to decode settings\") -> unwrap (errors.Unwrap) to get field-level cause; fail startup with the channel name in the message","preventionTips":["Run config through a validator/test loader in CI before deploy","Keep encrypted settings and their decryption key paired — never copy one without the other","After upgrades, re-decode all channel settings in a smoke test"],"tags":["registry","config","decoding","settings","startup"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}