{"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/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/boot_appearance.go#L202-L238","documentation":"SetBootAppearance requires the provider and appearanceID parameters to be consistent: both empty means 'reset to default appearance', both non-empty selects an appearance. Passing exactly one of them (an impossible half-selection) is rejected with this error before any validation or persistence happens.","triggerScenarios":"Calling SetBootAppearance(\"my-theme\", \"\") or SetBootAppearance(\"\", \"dark-splash\") — after trimming, exactly one argument is empty; API clients that omit one field in the JSON body so it defaults to empty string.","commonSituations":"Frontend form where only the provider dropdown was chosen but no appearance id; API scripts resetting only one half of the pair; JSON payloads with a missing key instead of an explicit empty string pair.","solutions":["Pass both provider and appearanceID, or pass both as empty strings to reset to the default appearance.","Fix API payloads to include both fields explicitly.","Validate the pair client-side before the call: (provider == \"\") == (appearanceID == \"\").","If only the appearance should change, look up the current provider first and send it together with the new id."],"exampleFix":"// before\nmodel.SetBootAppearance(\"my-theme\", \"\") // half-selection\n// after\nif reset { provider, id = \"\", \"\" }\nmodel.SetBootAppearance(provider, id) // both set or both empty","handlingStrategy":"validation","validationCode":"function isConsistentPair(provider, id) { return (provider === '') === (id === '') }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Send provider and appearanceID as an explicit pair, both empty or both set","In UIs, disable the confirm action until the pair is complete","Never omit one field in JSON payloads; use explicit empty strings for reset"],"tags":["boot-appearance","validation","mutually-dependent-args"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}