{"record":{"id":"6679615152b89518","repo":"Billionmail/BillionMail","slug":"error-getting-footer-config-v","errorCode":null,"errorMessage":"error getting footer config: %v","messagePattern":"error getting footer config: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":746,"sourceCode":"\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 != \"\" {\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)","sourceCodeStart":728,"sourceCodeEnd":764,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L728-L764","documentation":"ModifyFooter loads the current configuration by calling GetFooter(Domain) and wraps any error it returns. Because GetFooter both reads footer_config.json and auto-provisions a default when missing, this error can carry a provisioning write failure, a read failure, or an unmarshal failure nested inside it. The wrapped message identifies which stage failed.","triggerScenarios":"Calling ModifyFooter(Domain, CopyrightText, Disclaimer, Text) (also invoked recursively/indirectly per the call graph, e.g. from AutoGetProjectInfo) when footer_config.json is missing in a non-writable directory, unreadable, or contains invalid JSON.","commonSituations":"Production deployments with a read-only or unmounted config volume — every ModifyFooter call fails because GetFooter cannot provision the default; footer_config.json corrupted by a prior partial write; permissions changed by an external process.","solutions":["Unwrap the nested cause: 'error saving default footer config file' → create the domain directory (os.MkdirAll) or fix volume permissions; 'error reading' → fix file ownership; 'error unmarshalling' → repair or delete footer_config.json to force regeneration.","Verify PRODUCT_CONFIG_PATH/<Domain> exists and is writable by the service user in the deployed environment.","Check disk space and mount flags (df -h, mount) if writes fail on the host or in containers.","Once the file problem is fixed, retry ModifyFooter — no data loss occurs since the failed call made no changes."],"exampleFix":"// before\nif err := ModifyFooter(domain, \"© 2026 ACME\", \"\", \"\"); err != nil {\n    return err // opaque nested cause\n}\n// after\nif err := os.MkdirAll(filepath.Join(configPath, domain), 0755); err != nil {\n    return fmt.Errorf(\"prepare config dir: %w\", err)\n}\nif err := ModifyFooter(domain, \"© 2026 ACME\", \"\", \"\"); err != nil {\n    return fmt.Errorf(\"modify footer: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"dir := filepath.Join(configPath, domain)\nif err := os.MkdirAll(dir, 0755); err != nil { return err }\nif p := filepath.Join(dir, \"footer_config.json\"); f, err := os.Open(p); err == nil { f.Close() } else if !os.IsNotExist(err) { return err }","typeGuard":"func footerReady(domain string) bool {\n    dir := filepath.Join(configPath, domain)\n    if !writableDir(dir) { return false }\n    p := filepath.Join(dir, \"footer_config.json\")\n    if f, err := os.Open(p); err == nil { f.Close(); return true }\n    return os.IsNotExist(err) // GetFooter will provision\n}","tryCatchPattern":"if err := askai.ModifyFooter(domain, copyright, disclaimer, text); err != nil {\n    msg := err.Error()\n    switch {\n    case strings.Contains(msg, \"saving default footer config\"): // fix dir/perms, retry\n    case strings.Contains(msg, \"unmarshalling footer config\"): // repair or delete file, retry\n    default: return err\n    }\n}","preventionTips":["Ensure the domain config directory exists and is writable before calling ModifyFooter.","Unwrap nested errors with errors.Is/errors.As instead of string matching in production code.","Monitor for read-only or full config volumes in deployments.","Delete corrupt footer_config.json to let GetFooter regenerate defaults."],"tags":["go","config","file-io","footer","error-wrapping","askai"],"backgroundTag":"config-file-write-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"}