{"record":{"id":"b8a749dd4521af57","repo":"IceWhaleTech/CasaOS","slug":"mount-failed","errorCode":null,"errorMessage":"mount failed","messagePattern":"mount failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/utils/httper/drive.go","lineNumber":91,"sourceCode":"\tjson.Unmarshal(res.Body(), &result)\n\tfor i := 0; i < len(result.MountPoints); i++ {\n\t\tresult.MountPoints[i].Fs = result.MountPoints[i].Fs[:len(result.MountPoints[i].Fs)-1]\n\t}\n\treturn result, err\n}\n\nfunc Mount(mountPoint string, fs string) error {\n\tres, err := NewRestyClient().R().SetFormData(map[string]string{\n\t\t\"mountPoint\": mountPoint,\n\t\t\"fs\":         fs,\n\t\t\"mountOpt\":   `{\"AllowOther\": true}`,\n\t\t\"vfsOpt\":     `{\"CacheMode\": 3}`,\n\t}).Post(\"/mount/mount\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res.StatusCode() != 200 {\n\t\treturn fmt.Errorf(\"mount failed\")\n\t}\n\tlogger.Info(\"mount then\", zap.Any(\"res\", res.Body()))\n\treturn nil\n}\nfunc Unmount(mountPoint string) error {\n\tres, err := NewRestyClient().R().SetFormData(map[string]string{\n\t\t\"mountPoint\": mountPoint,\n\t}).Post(\"/mount/unmount\")\n\tif err != nil {\n\t\tlogger.Error(\"when unmount\", zap.Error(err))\n\t\treturn err\n\t}\n\tif res.StatusCode() != 200 {\n\t\tlogger.Error(\"then unmount failed\", zap.Any(\"res\", res.Body()))\n\t\treturn fmt.Errorf(\"unmount failed\")\n\t}\n\n\tlogger.Info(\"unmount then\", zap.Any(\"res\", res.Body()))","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/IceWhaleTech/CasaOS/blob/0d3b2f444ec0193193cf03eef6d43c6e35b0183e/pkg/utils/httper/drive.go#L73-L109","documentation":"Returned by Mount() in pkg/utils/httper/drive.go when the POST to the rclone REST endpoint /mount/mount completes but responds with a non-200 status. The CasaOS 'drive' layer is a thin HTTP wrapper over rclone's librclone/rest API, so a non-200 means rclone itself refused the mount (bad fs name, invalid options, or busy mount point). The error carries no detail because the code formats a static string and discards res.Body(), where rclone puts the actual reason.","triggerScenarios":"POST /mount/mount with form fields mountPoint, fs, mountOpt={\"AllowOther\":true}, vfsOpt={\"CacheMode\":3} against the rclone service returns non-200: (1) fs names a config that does not exist in rclone, (2) mountPoint path does not exist or is already mounted, (3) AllowOther requires user_allow_other in /etc/fuse.conf and it is disabled, (4) the rclone REST service is being restarted concurrently.","commonSituations":"Drive feature of CasaOS after a factory reset where rclone configs were wiped; upgrading rclone changed option names so previously valid mountOpt/vfsOpt JSON is rejected; running in a container without /dev/fuse or with FUSE not installed; leftover stale mount from a crashed process blocking the same mountPoint.","solutions":["Check the rclone/drive service is running and healthy (systemctl status or the CasaOS service list) before calling Mount.","Verify the fs config exists first with GetConfigByName(name) and that its type/backend parameters are valid.","Confirm the mountPoint directory exists, is empty, and is not already mounted (mountpoint -q /mnt point).","Ensure /etc/fuse.conf contains user_allow_other, since mountOpt sends AllowOther:true.","Improve the error to include res.StatusCode() and string(res.Body()) so rclone's actual refusal reason is visible."],"exampleFix":"// before\nif res.StatusCode() != 200 {\n    return fmt.Errorf(\"mount failed\")\n}\n\n// after\nif res.StatusCode() != 200 {\n    return fmt.Errorf(\"mount failed: status=%d body=%s\", res.StatusCode(), res.Body())\n}","handlingStrategy":"validation","validationCode":"// before calling Mount()\nif _, err := httper.GetConfigByName(fs); err != nil {\n    return fmt.Errorf(\"fs %q not configured: %w\", fs, err)\n}\nif err := unix.Access(mountPoint, unix.W_OK); err != nil {\n    return fmt.Errorf(\"mount point %q not writable: %w\", mountPoint, err)\n}\n// reject an already-mounted point\nif mnts, _ := os.ReadFile(\"/proc/mounts\"); strings.Contains(string(mnts), \" \"+mountPoint+\" \") {\n    return fmt.Errorf(\"%s already mounted\", mountPoint)\n}","typeGuard":null,"tryCatchPattern":"if err := httper.Mount(point, fs); err != nil {\n    if strings.Contains(err.Error(), \"mount failed\") {\n        // rclone refused: surface body detail, do not blind-retry (option errors are deterministic)\n        logger.Error(\"rclone refused mount\", zap.String(\"fs\", fs), zap.Error(err))\n        return err\n    }\n    // transport error: retry once after confirming the drive service is up\n    return retryOnce(func() error { return httper.Mount(point, fs) })\n}","preventionTips":["Create and validate the rclone config before ever calling Mount.","Keep mountPoint paths canonical (filepath.Clean) and store them so mount/unmount use identical strings.","Enable user_allow_other in /etc/fuse.conf at image/provisioning time, since mountOpt always sends AllowOther:true.","Wrap drive helpers so status and response body are always included in returned errors."],"tags":["rclone","mount","fuse","http-client","casaos"],"backgroundTag":null,"analyzedSha":"0d3b2f444ec0193193cf03eef6d43c6e35b0183e","analyzedAt":"2026-08-15T13:27:57.821Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}