{"record":{"id":"4b0b11e7a54ac8ef","repo":"siyuan-note/siyuan","slug":"provider-and-appearance-must-both-be-empty-or-non","errorCode":null,"errorMessage":"provider and appearance must both be empty or non-empty","messagePattern":"provider and appearance must both be empty or non-empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/boot_appearance.go","lineNumber":220,"sourceCode":"\tif err != nil {\n\t\treturn &BootAppearance{Enabled: false}\n\t}\n\tif selection.Provider == \"\" || selection.Appearance == \"\" {\n\t\treturn &BootAppearance{Enabled: false}\n\t}\n\treturn appearance\n}\n\n// SetBootAppearance 校验并原子持久化启动页外观选择，两项均为空表示恢复默认。\nfunc SetBootAppearance(provider, appearanceID string) (ret BootAppearanceSelection, err error) {\n\tif util.ReadOnly {\n\t\treturn defaultBootAppearanceSelection(), errors.New(\"read-only mode\")\n\t}\n\tprovider = strings.TrimSpace(provider)\n\tappearanceID = strings.TrimSpace(appearanceID)\n\tret = defaultBootAppearanceSelection()\n\tif (provider == \"\") != (appearanceID == \"\") {\n\t\terr = errors.New(\"provider and appearance must both be empty or non-empty\")\n\t\treturn\n\t}\n\tif provider != \"\" {\n\t\tif !bazaar.IsValidPackageName(provider) || !isValidBootAppearanceID(appearanceID) {\n\t\t\terr = ErrBootAppearanceNotFound\n\t\t\treturn\n\t\t}\n\t\tif _, resolveErr := getBootAppearanceByID(provider, appearanceID); resolveErr != nil {\n\t\t\terr = ErrBootAppearanceNotFound\n\t\t\treturn\n\t\t}\n\t\tret.Provider, ret.Appearance = provider, appearanceID\n\t}\n\n\tdata, err := gulu.JSON.MarshalIndentJSON(ret, \"\", \"\\t\")\n\tif err != nil {\n\t\treturn ret, err\n\t}","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/boot_appearance.go#L202-L238","documentation":"SetBootAppearance requires the provider and appearanceID to be consistent: either both empty (restore default) or both non-empty (select an appearance). A half-specified selection is ambiguous, so it is rejected with 'provider and appearance must both be empty or non-empty'.","triggerScenarios":"Calling SetBootAppearance with exactly one of the two arguments set — e.g. provider set but appearanceID \"\" (or vice versa) after trimming.","commonSituations":"API callers that map two optional query params independently and pass through unset ones as empty strings; UI code updating only one field at a time.","solutions":["Always send both fields together: both empty to reset, both filled to select","Coalesce in the caller: if either field is empty, treat the request as a reset (pass both as \"\")","Fix UI/API binding so the two parameters are not partially populated"],"exampleFix":"// before\nmodel.SetBootAppearance(\"my-plugin\", \"\") // mismatched\n// after\nif (provider == \"\") != (appearanceID == \"\") { provider, appearanceID = \"\", \"\" } // reset\nmodel.SetBootAppearance(provider, appearanceID)","handlingStrategy":"validation","validationCode":"if (provider == \"\") != (appearanceID == \"\") { return errors.New(\"provider and appearance must both be set or both empty\") }","typeGuard":"func paired(p, a string) (string, string, bool) { if (p == \"\") != (a == \"\") { return \"\", \"\", false }; return p, a, true }","tryCatchPattern":"if _, err := model.SetBootAppearance(p, a); err != nil && strings.Contains(err.Error(), \"both be empty or non-empty\") {\n  // fix the pairing and retry or reset\n}","preventionTips":["Send both fields as one unit from the UI/API layer","Default missing fields to \"\" together, never independently","Treat selection as a single (provider, appearance) tuple"],"tags":["go","kernel","validation","boot-appearance"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}