{"record":{"id":"3ddb271d180edfca","repo":"Billionmail/BillionMail","slug":"error-unmarshalling-footer-config-v","errorCode":null,"errorMessage":"error unmarshalling footer config: %v","messagePattern":"error unmarshalling footer config: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":735,"sourceCode":"\t\tif err != nil {\n\t\t\treturn FooterConfig{}, fmt.Errorf(\"error marshalling default footer config: %v\", err)\n\t\t}\n\t\terr = os.WriteFile(filename, defaultFooterJson, 0644)\n\t\tif err != nil {\n\t\t\treturn FooterConfig{}, fmt.Errorf(\"error saving default footer config file: %v\", err)\n\t\t}\n\t\t// Return the default footer config if the file does not exist\n\t\treturn defaultFooterConfig, nil\n\t}\n\tdata, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn FooterConfig{}, fmt.Errorf(\"error reading footer config file: %v\", err)\n\t}\n\n\tvar config FooterConfig\n\terr = json.Unmarshal(data, &config)\n\tif err != nil {\n\t\treturn FooterConfig{}, fmt.Errorf(\"error unmarshalling footer config: %v\", err)\n\t}\n\treturn config, nil\n}\n\n// ModifyFooter updates the footer configuration for a given domain.\n// It reads the existing footer configuration, modifies the specified fields, and saves the updated configuration back to the file.\n// If any field is empty, it will not be updated.\nfunc ModifyFooter(Domain string, CopyrightText, Disclaimer string, Text string) error {\n\tconfig, err := GetFooter(Domain)\n\tif err != nil {\n\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 != \"\" {","sourceCodeStart":717,"sourceCodeEnd":753,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L717-L753","documentation":"After reading footer_config.json, GetFooter unmarshals it into FooterConfig with json.Unmarshal and wraps any decoding failure. This means the file exists and is readable, but its bytes are not valid JSON or do not match FooterConfig's schema (wrong types, e.g. a number where a string is expected). Commonly the file was corrupted by a partial write or edited by hand.","triggerScenarios":"Calling GetFooter when footer_config.json contains truncated/corrupt JSON (e.g. an interrupted WriteFile or disk-full during provisioning), hand-edited invalid JSON, or a schema mismatch such as \"updateTime\": 123 where a string is required.","commonSituations":"A crash or OOM kill during the initial default-config write left a partial file; an operator edited footer_config.json without validating; an older app version wrote a differently-typed field that the current FooterConfig struct rejects; config copied from another tool with a different format.","solutions":["Validate the file: run it through jq or json.Valid to see the exact syntax error reported in the wrapped message.","Repair the JSON by hand or restore from backup; worst case, delete footer_config.json so GetFooter regenerates a valid default on next call.","If the wrapped error mentions 'cannot unmarshal ... into Go struct field', align the JSON value types with the FooterConfig struct fields (or vice versa).","Harden writes (write to temp file + os.Rename) so a crash can never leave a truncated footer_config.json."],"exampleFix":"// before\nerr = os.WriteFile(filename, data, 0644)\n// after\ntmp := filename + \".tmp\"\nif err := os.WriteFile(tmp, data, 0644); err != nil { return err }\nif err := os.Rename(tmp, filename); err != nil { return err } // atomic, avoids corrupt partial files","handlingStrategy":"validation","validationCode":"data, err := os.ReadFile(footerPath)\nif err == nil && !json.Valid(data) {\n    os.Remove(footerPath) // let GetFooter regenerate a valid default\n}","typeGuard":"func footerConfigFileValid(path string) bool {\n    data, err := os.ReadFile(path)\n    if err != nil { return false }\n    var c map[string]any\n    return json.Unmarshal(data, &c) == nil\n}","tryCatchPattern":"if _, err := askai.GetFooter(domain); err != nil {\n    if strings.Contains(err.Error(), \"unmarshalling footer config\") {\n        os.Remove(footerPath) // corrupt: regenerate default, retry once\n        if cfg, retryErr := askai.GetFooter(domain); retryErr == nil { _ = cfg }\n    }\n    return err\n}","preventionTips":["Validate JSON after every manual edit to footer_config.json.","Use atomic writes (temp + rename) so crashes cannot leave truncated configs.","Keep FooterConfig field types stable across versions or add migration handling.","Alert on the corrupt file rather than silently retrying forever."],"tags":["go","json","config","corrupt-file","footer","askai"],"backgroundTag":"json-unmarshal-failed","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"}