{"record":{"id":"74867d4a4abfaa15","repo":"usememos/memos","slug":"storage-id-is-required","errorCode":null,"errorMessage":"storage ID is required","messagePattern":"storage ID is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"store/storage.go","lineNumber":465,"sourceCode":"\t\tsetting.S3Config = proto.CloneOf(storage.GetS3Config())\n\t\treturn\n\t}\n\t// Keep the most recently configured S3 storage available to legacy\n\t// attachments that predate both storage IDs and embedded configurations.\n\tfor _, configuredStorage := range setting.Storages {\n\t\tif configuredStorage.GetS3Config() != nil {\n\t\t\tsetting.S3Config = proto.CloneOf(configuredStorage.GetS3Config())\n\t\t\treturn\n\t\t}\n\t}\n\tsetting.S3Config = nil\n}\n\nfunc validateInstanceStorages(setting *storepb.InstanceStorageSetting) error {\n\tseenIDs := map[string]bool{}\n\tfor _, storage := range setting.Storages {\n\t\tif storage == nil || storage.Id == \"\" {\n\t\t\treturn errors.New(\"storage ID is required\")\n\t\t}\n\t\tif seenIDs[storage.Id] {\n\t\t\treturn errors.Errorf(\"duplicate storage ID %q\", storage.Id)\n\t\t}\n\t\tseenIDs[storage.Id] = true\n\t\tif storage.Type == storepb.StorageType_STORAGE_TYPE_UNSPECIFIED {\n\t\t\treturn errors.Errorf(\"storage %q type is required\", storage.Id)\n\t\t}\n\t\tif storage.Type == storepb.StorageType_STORAGE_TYPE_S3 && storage.GetS3Config() == nil {\n\t\t\treturn errors.Errorf(\"storage %q S3 config is required\", storage.Id)\n\t\t}\n\t}\n\tif !seenIDs[setting.DefaultStorageId] {\n\t\treturn errors.Errorf(\"default storage %q is not configured\", setting.DefaultStorageId)\n\t}\n\treturn nil\n}\n","sourceCodeStart":447,"sourceCodeEnd":483,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/store/storage.go#L447-L483","documentation":"validateInstanceStorages (store/storage.go:464) checks each entry of setting.Storages during a storage-setting update; an entry that is nil or has an empty Id fails with errors.New(\"storage ID is required\"). Storage IDs are the stable identity used by DefaultStorageId, attachment references, and namespace-change re-identification, so an ID-less entry would break resolution. NormalizeInstanceStorageSetting normally guarantees IDs, which is why only raw canonical update requests are validated explicitly (storage.go:107-115).","triggerScenarios":"Sending an UpdateWorkspaceSetting storage request whose storages[] includes a message with no id field (empty string) or a null list element; constructing an InstanceStorageSetting programmatically and appending an unconfigured &storepb.Storage{}.","commonSituations":"Clients that omit id assuming the server generates one; hand-built protobuf/JSON payloads from scripts or Terraform-like tooling; partial request snapshots replayed after a proto schema change renamed the id field; older client versions unaware that named storages require explicit IDs.","solutions":["Give every storages[] entry a non-empty id (e.g. \"s3-primary\", \"local\") before sending the update; keep IDs stable across edits so attachments keep resolving.","Send the canonical update path (storages list populated) rather than relying on legacy normalization, and validate IDs client-side first.","After upgrading the server, re-fetch the current setting and echo its storage IDs back instead of constructing them from scratch."],"exampleFix":"// before\nsetting.Storages = append(setting.Storages, &storepb.Storage{\n    Type:   storepb.StorageType_STORAGE_TYPE_S3,\n    Config: &storepb.Storage_S3Config{S3Config: cfg}, // no Id\n})\n\n// after\nsetting.Storages = append(setting.Storages, &storepb.Storage{\n    Id:     \"s3-primary\",\n    Type:   storepb.StorageType_STORAGE_TYPE_S3,\n    Config: &storepb.Storage_S3Config{S3Config: cfg},\n})","handlingStrategy":"validation","validationCode":"func validStorages(setting *storepb.InstanceStorageSetting) error {\n    seen := map[string]bool{}\n    for _, s := range setting.GetStorages() {\n        if s == nil || s.GetId() == \"\" {\n            return errors.New(\"storage ID is required\")\n        }\n        if seen[s.GetId()] {\n            return errors.Errorf(\"duplicate storage ID %q\", s.GetId())\n        }\n        seen[s.GetId()] = true\n    }\n    return nil\n}\n// run before PrepareInstanceStorageSettingUpdate / the update request","typeGuard":"func hasAllStorageIDs(setting *storepb.InstanceStorageSetting) bool {\n    for _, s := range setting.GetStorages() {\n        if s == nil || s.GetId() == \"\" {\n            return false\n        }\n    }\n    return true\n}","tryCatchPattern":null,"preventionTips":["Always assign a human-stable id when adding a storages[] entry; never rely on the server to invent one.","Echo IDs from a fresh GetWorkspaceSetting when editing, instead of hand-building the list.","Validate the setting client-side with the same rules (non-empty unique IDs, type set, S3 config present for S3) to catch errors before the round trip."],"tags":["storage","validation","workspace-setting","protobuf"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}