{"record":{"id":"44e3e4ec525feaf3","repo":"usememos/memos","slug":"storagesetting-s3config-is-required-for-s3","errorCode":null,"errorMessage":"storageSetting.s3Config is required for S3","messagePattern":"storageSetting\\.s3Config is required for S3","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"store/deployment_config.go","lineNumber":239,"sourceCode":"\nfunc validateAndNormalizeDeploymentInstanceSetting(setting *storepb.InstanceSetting) error {\n\tswitch setting.Key {\n\tcase storepb.InstanceSettingKey_GENERAL:\n\t\tif setting.GetGeneralSetting() == nil {\n\t\t\treturn errors.New(\"generalSetting must be populated for key GENERAL\")\n\t\t}\n\t\tif offset := setting.GetGeneralSetting().WeekStartDayOffset; offset < -1 || offset > 6 {\n\t\t\treturn errors.New(\"generalSetting.weekStartDayOffset must be between -1 and 6\")\n\t\t}\n\tcase storepb.InstanceSettingKey_STORAGE:\n\t\tstorage := setting.GetStorageSetting()\n\t\tif storage == nil {\n\t\t\treturn errors.New(\"storageSetting must be populated for key STORAGE\")\n\t\t}\n\t\t// Normalization would silently self-heal this misconfiguration to LOCAL;\n\t\t// a deployment file declaring S3 without a config should fail loudly.\n\t\tif storage.StorageType == storepb.InstanceStorageSetting_S3 && storage.S3Config == nil && len(storage.Storages) == 0 {\n\t\t\treturn errors.New(\"storageSetting.s3Config is required for S3\")\n\t\t}\n\t\tNormalizeInstanceStorageSetting(storage)\n\t\tif storage.UploadSizeLimitMb < 0 {\n\t\t\treturn errors.New(\"storageSetting.uploadSizeLimitMb must not be negative\")\n\t\t}\n\t\tdefaultStorage := GetDefaultStorage(storage)\n\t\tif defaultStorage != nil && defaultStorage.Type == storepb.StorageType_STORAGE_TYPE_S3 {\n\t\t\ts3Config := defaultStorage.GetS3Config()\n\t\t\tif s3Config == nil {\n\t\t\t\treturn errors.New(\"storageSetting default storage S3 config is required\")\n\t\t\t}\n\t\t\tfor _, field := range []struct {\n\t\t\t\tname  string\n\t\t\t\tvalue string\n\t\t\t}{\n\t\t\t\t{name: \"accessKeyId\", value: s3Config.AccessKeyId},\n\t\t\t\t{name: \"accessKeySecret\", value: s3Config.AccessKeySecret},\n\t\t\t\t{name: \"endpoint\", value: s3Config.Endpoint},","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/store/deployment_config.go#L221-L257","documentation":"Thrown when a STORAGE instance-setting file sets storageType to S3 but provides neither s3Config nor any entries in the storages list. The code comment explains the intent: normalization would silently self-heal this to LOCAL storage, so the validator fails loudly instead, preventing an operator's S3 data from silently going to local disk. Startup aborts.","triggerScenarios":"A memos-instance-setting-storage.json with \"storageType\": \"S3\" and no \"s3Config\" block and empty/missing \"storages\" array; partially migrated S3 config where credentials moved into storages[] but the array got dropped.","commonSituations":"Declaring intent (\"we use S3\") before filling in credentials; downgrading from a version that stored S3 config in the database and assuming the server will read it from there; typo in the s3Config key leaving it unset.","solutions":["Provide the S3 config inline: add \"s3Config\": { \"accessKeyId\": \"...\", \"accessKeySecret\": \"...\", \"endpoint\": \"...\", \"region\": \"...\", \"bucket\": \"...\" } to storageSetting.","Or declare the S3 storage under storageSetting.storages[] with a matching id/actived state.","Or, if local storage is actually intended, set \"storageType\": \"LOCAL\"."],"exampleFix":"// before\n{ \"key\": \"STORAGE\", \"storageSetting\": { \"storageType\": \"S3\", \"uploadSizeLimitMb\": 100 } }\n\n// after\n{ \"key\": \"STORAGE\", \"storageSetting\": { \"storageType\": \"S3\", \"uploadSizeLimitMb\": 100,\n  \"s3Config\": { \"accessKeyId\": \"AKIA...\", \"accessKeySecret\": \"...\", \"endpoint\": \"https://s3.amazonaws.com\", \"region\": \"us-east-1\", \"bucket\": \"memos\" } } }","handlingStrategy":"validation","validationCode":"st := setting.GetStorageSetting()\nif st.StorageType == storepb.InstanceStorageSetting_S3 && st.S3Config == nil && len(st.Storages) == 0 {\n    return errors.New(\"S3 storageType requires s3Config or storages entries\")\n}","typeGuard":"func s3StorageIsConfigured(st *storepb.InstanceStorageSetting) bool {\n    if st.GetStorageType() != storepb.InstanceStorageSetting_S3 {\n        return true\n    }\n    return st.GetS3Config() != nil || len(st.GetStorages()) > 0\n}","tryCatchPattern":null,"preventionTips":["Treat storageType as a commitment: only declare S3 when the full s3Config is mounted in the same file.","Mount S3 credentials as a separate secret file and reference it in your config generator, so a missing secret fails your pipeline instead of server startup."],"tags":["configuration","validation","storage","s3","startup"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}