{"record":{"id":"58acd3c908ebc025","repo":"Billionmail/BillionMail","slug":"error-saving-default-footer-config-file-v","errorCode":null,"errorMessage":"error saving default footer config file: %v","messagePattern":"error saving default footer config file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":722,"sourceCode":"\tfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/footer_config.json\", Domain)\n\tif !public.FileExists(filename) {\n\t\t// If the footer config file does not exist, return a default footer config\n\t\t// This allows the system to handle cases where the footer 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 footer configuration without needing to handle file\n\t\t// not found errors.\n\t\tdefaultFooterConfig := FooterConfig{\n\t\t\tCopyrightText: \"© 2023 Your Company. All rights reserved.\",\n\t\t\tDisclaimer:    \"This is a sample disclaimer.\",\n\t\t}\n\t\tdefaultFooterConfig.UpdateTime = public.GetNowTime()\n\t\tdefaultFooterJson, err := json.MarshalIndent(defaultFooterConfig, \"\", \"  \")\n\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.","sourceCodeStart":704,"sourceCodeEnd":740,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L704-L740","documentation":"When footer_config.json is missing, GetFooter writes a freshly marshalled default config to PRODUCT_CONFIG_PATH/<Domain>/footer_config.json via os.WriteFile (0644) so subsequent reads succeed. This error wraps any filesystem failure during that provisioning write: missing domain directory, permissions, read-only filesystem, or disk full.","triggerScenarios":"First GetFooter call for a domain whose PRODUCT_CONFIG_PATH/<Domain>/ directory does not exist or is not writable by the service user; also triggered by ModifyFooter and GetFooterPrompt as they route through GetFooter.","commonSituations":"New domain provisioned in the app but its config directory never created in the container/volume; config volume mounted read-only in production; service runs as non-root but the config dir was created by root with 0755 root-owned perms; disk-full on the host.","solutions":["If the wrapped error is 'no such file or directory', create the domain config directory: os.MkdirAll(filepath.Dir(filename), 0755) before the WriteFile.","If 'permission denied', chown/chmod PRODUCT_CONFIG_PATH/<Domain> so the running user can write, or align the container's UID with the volume owner.","Check whether the config volume is mounted read-only and remount it rw; verify disk space.","Ensure PRODUCT_CONFIG_PATH resolves to the persistent volume in the deployed environment, not an ephemeral image path."],"exampleFix":"// before\nerr = os.WriteFile(filename, defaultFooterJson, 0644)\n// after\nif mkErr := os.MkdirAll(filepath.Dir(filename), 0755); mkErr != nil {\n    return FooterConfig{}, fmt.Errorf(\"error creating footer config directory: %v\", mkErr)\n}\nerr = os.WriteFile(filename, defaultFooterJson, 0644)","handlingStrategy":"validation","validationCode":"dir := filepath.Join(configPath, domain)\nif err := os.MkdirAll(dir, 0755); err != nil { return err }\nif f, err := os.OpenFile(filepath.Join(dir, \".probe\"), os.O_CREATE|os.O_WRONLY, 0644); err != nil { return err } else { f.Close(); os.Remove(filepath.Join(dir, \".probe\")) }","typeGuard":"func canProvisionDefault(domain string) bool {\n    return writableDir(filepath.Join(configPath, domain))\n}","tryCatchPattern":"if _, err := askai.GetFooter(domain); err != nil {\n    if strings.Contains(err.Error(), \"saving default footer config\") {\n        // provisioning failed: create dir / fix perms / remount rw, then retry\n    } else { return err }\n}","preventionTips":["Create the domain config directory during domain provisioning, not lazily.","Deploy with the config volume mounted read-write and correct UID mapping.","Alert on disk-full; GetFooter provisioning is the first casualty."],"tags":["go","filesystem","permissions","config","footer","askai"],"backgroundTag":"permission-denied","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-15T02:17:10.978Z"}