{"record":{"id":"259560d9496f1f75","repo":"siyuan-note/siyuan","slug":"update-channel-is-invalid-259560","errorCode":null,"errorMessage":"update channel is invalid","messagePattern":"update channel is invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/updater_release.go","lineNumber":87,"sourceCode":"\tDigest             string `json:\"digest\"`\n\tState              string `json:\"state\"`\n}\n\nvar (\n\tcachedGitHubReleases    []*githubRelease\n\tgithubReleasesCacheTime int64\n\tgithubReleasesLock      sync.RWMutex\n\tgithubReleasesFlight    singleflight.Group\n\tgithubManifestCache     sync.Map\n)\n\nfunc getUpdateRelease(force bool) (*updateRelease, error) {\n\tchannel := Conf.System.UpdateChannel\n\tif conf.UpdateChannelStable == channel {\n\t\treturn getStableUpdateRelease(force)\n\t}\n\tif !isValidUpdateChannel(channel) {\n\t\treturn nil, errors.New(\"update channel is invalid\")\n\t}\n\treturn getGitHubUpdateRelease(channel, force)\n}\n\nfunc getStableUpdateRelease(force bool) (*updateRelease, error) {\n\tresult, err := util.GetRhyResult(context.TODO(), force)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tversion, ok := result[\"ver\"].(string)\n\tnormalizedVersion := normalizeReleaseVersion(version)\n\tif !ok || !semver.IsValid(normalizedVersion) || \"\" != semver.Prerelease(normalizedVersion) {\n\t\treturn nil, errors.New(\"stable release version is invalid\")\n\t}\n\tversion = strings.TrimPrefix(normalizedVersion, \"v\")\n\n\trelease := &updateRelease{\n\t\tVersion:    version,","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/updater_release.go#L69-L105","documentation":"Returned by getUpdateRelease(force) when Conf.System.UpdateChannel is neither UpdateChannelStable nor a value accepted by isValidUpdateChannel. This is the read-path counterpart to error 940: it guards every release lookup. Unlike SetUpdateChannel (which is user-driven), this fires when the in-memory config holds a corrupted channel string.","triggerScenarios":"Conf.System.UpdateChannel was set to an invalid value by direct struct mutation bypassing SetUpdateChannel. A config file (conf.json) was hand-edited with a bad channel and loaded at boot. A future code path assigns the channel without validation.","commonSituations":"Manually editing the workspace conf.json and misspelling the channel. A plugin writes to Conf.System directly. Migration/upgrade code that populated the field incorrectly.","solutions":["Reset the channel via the settings UI or by calling SetUpdateChannel(conf.UpdateChannelStable).","Fix the conf.json file in the workspace so system.updateChannel is exactly \"stable\", \"beta\", or \"alpha\".","Call loadGlobalUpdateChannel() to fall back to the per-user ~/.config/siyuan/update.json, which self-heals invalid values to stable (see lines 50-52)."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Validate the in-memory channel before any release lookup.\nif !isValidUpdateChannel(Conf.System.UpdateChannel) {\n    Conf.System.UpdateChannel = conf.UpdateChannelStable // self-heal\n}","typeGuard":null,"tryCatchPattern":"// Fall back to stable on the read path.\nrelease, err := getUpdateRelease(false)\nif err != nil && strings.Contains(err.Error(), \"update channel is invalid\") {\n    Conf.System.UpdateChannel = conf.UpdateChannelStable\n    release, err = getUpdateRelease(false)\n}","preventionTips":["Never assign Conf.System.UpdateChannel directly — always go through SetUpdateChannel.","On boot, sanitize the field with isValidUpdateChannel and reset to stable if bad.","Avoid hand-editing conf.json for the channel; use the settings UI."],"tags":["config","update","channel","validation"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}