{"record":{"id":"c6365e070e2026f1","repo":"Billionmail/BillionMail","slug":"error-marshalling-footer-config-v","errorCode":null,"errorMessage":"error marshalling footer config: %v","messagePattern":"error marshalling footer config: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":764,"sourceCode":"\t\treturn fmt.Errorf(\"error getting footer config: %v\", err)\n\t}\n\n\t// Update the footer configuration\n\tif CopyrightText != \"\" {\n\t\tconfig.CopyrightText = CopyrightText\n\t}\n\tif Disclaimer != \"\" {\n\t\tconfig.Disclaimer = Disclaimer\n\t}\n\tif Text != \"\" {\n\t\tconfig.Text = Text\n\t}\n\n\tconfig.UpdateTime = public.GetNowTime()\n\tfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/footer_config.json\", Domain)\n\tdata, err := json.Marshal(config)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling footer config: %v\", err)\n\t}\n\terr = os.WriteFile(filename, data, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error saving footer config file: %v\", err)\n\t}\n\treturn nil\n}\n\n// GetPrompt retrieves the prompt configuration for a given domain from a JSON file.\n// It reads the prompt configuration file and returns a PromptConfig struct.\n// If the file does not exist or cannot be read, it returns an error.\nfunc GetPrompt(Domain string) (PromptConfig, error) {\n\tfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/prompt_config.json\", Domain)\n\tif !public.FileExists(filename) {\n\t\t// If the prompt config file does not exist, return a default prompt config\n\t\t// This allows the system to handle cases where the prompt configuration has not been set up\n\t\t// and avoids errors when trying to read a non-existent file.\n\t\t// It also allows the user to create a new prompt configuration without needing to handle file","sourceCodeStart":746,"sourceCodeEnd":782,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L746-L782","documentation":"ModifyFooter in core/internal/service/askai/project.go:764 fails when json.Marshal cannot serialize the updated FooterConfig struct before it is written to PRODUCT_CONFIG_PATH/<domain>/footer_config.json. Marshal errors are rare for plain config structs and almost always indicate an unsupported value (channel, func, cyclic pointer) embedded in the struct. The wrapped json.MarshalTypeError is returned so the caller can surface why serialization failed.","triggerScenarios":"Calling ModifyFooter(domain, copyrightText, disclaimer, text) when the loaded FooterConfig (from GetFooter) contains a field json.Marshal cannot encode: an unsupported type (func, chan, complex), a custom MarshalJSON method that returns an error, or a cyclic pointer/graph (unsupported recursively).","commonSituations":"A developer extends FooterConfig with a non-serializable field (e.g., a callback or time.Time with a broken custom marshaler), or a custom MarshalJSON on FooterConfig/fields returns an error on the current data, or a data race corrupts pointer fields into a cycle between GetFooter and Marshal.","solutions":["Inspect the wrapped error (%v) for the json.MarshalTypeError field/type to identify the offending FooterConfig field.","Remove or make serializable any func/chan/complex fields added to FooterConfig; encode them as strings or IDs.","Check custom MarshalJSON implementations on FooterConfig and its field types for error paths.","If a cycle is suspected, add pointer guards or store plain values instead of shared pointers."],"exampleFix":"// before\ntype FooterConfig struct {\n    Text string\n    OnSave func() // unsupported by json.Marshal\n}\n// after\ntype FooterConfig struct {\n    Text string\n    OnSaveName string `json:\"onSaveName\"` // serializable representation\n}","handlingStrategy":"try-catch","validationCode":"// Go: validate serializability before calling ModifyFooter\nfunc footerConfigSerializable(domain string) error {\n    cfg, err := GetFooter(domain)\n    if err != nil {\n        return err\n    }\n    _, err = json.Marshal(cfg)\n    return err\n}","typeGuard":"func isMarshalable(v any) bool {\n    _, err := json.Marshal(v)\n    return err == nil\n}","tryCatchPattern":"if err := askai.ModifyFooter(domain, copyright, disclaimer, text); err != nil {\n    var mte *json.MarshalTypeError\n    if errors.As(err, &mte) {\n        log.Printf(\"footer config field %s (%s) not serializable\", mte.Field, mte.Type)\n    } else {\n        log.Printf(\"modify footer failed: %v\", err)\n    }\n    return err\n}","preventionTips":["Keep FooterConfig limited to JSON-serializable field types (strings, numbers, bools).","Add a unit test marshalling a representative FooterConfig instance.","Avoid custom MarshalJSON implementations unless thoroughly tested.","Never store funcs, channels, or shared cyclic pointers in config structs."],"tags":["go","json","serialization","config"],"backgroundTag":"json-marshal-unsupported-type","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}