{"record":{"id":"ac415c0a580d58c7","repo":"vitessio/vitess","slug":"endbackup-cannot-be-called-on-read-only-backup-ac415c","errorCode":null,"errorMessage":"EndBackup cannot be called on read-only backup","messagePattern":"EndBackup cannot be called on read-only backup","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/vt/mysqlctl/gcsbackupstorage/gcs.go","lineNumber":98,"sourceCode":"\treturn bh.name\n}\n\n// AddFile implements BackupHandle.\nfunc (bh *GCSBackupHandle) AddFile(ctx context.Context, filename string, filesize int64) (io.WriteCloser, error) {\n\tif bh.readOnly {\n\t\treturn nil, errors.New(\"AddFile cannot be called on read-only backup\")\n\t}\n\tobject := objName(bh.dir, bh.name, filename)\n\treturn bh.client.Bucket(bucket).Object(object).NewWriter(ctx), nil\n}\n\n// Wait implements BackupHandle.\nfunc (bh *GCSBackupHandle) Wait() {}\n\n// EndBackup implements BackupHandle.\nfunc (bh *GCSBackupHandle) EndBackup(ctx context.Context) error {\n\tif bh.readOnly {\n\t\treturn errors.New(\"EndBackup cannot be called on read-only backup\")\n\t}\n\treturn nil\n}\n\n// AbortBackup implements BackupHandle.\nfunc (bh *GCSBackupHandle) AbortBackup(ctx context.Context) error {\n\tif bh.readOnly {\n\t\treturn errors.New(\"AbortBackup cannot be called on read-only backup\")\n\t}\n\treturn bh.bs.RemoveBackup(ctx, bh.dir, bh.name)\n}\n\n// ReadFile implements BackupHandle.\nfunc (bh *GCSBackupHandle) ReadFile(ctx context.Context, filename string) (io.ReadCloser, error) {\n\tif !bh.readOnly {\n\t\treturn nil, errors.New(\"ReadFile cannot be called on read-write backup\")\n\t}\n\tobject := objName(bh.dir, bh.name, filename)","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/mysqlctl/gcsbackupstorage/gcs.go#L80-L116","documentation":"GCSBackupHandle.EndBackup refuses to finalize a backup that was opened read-only. EndBackup is the completion step of the write lifecycle and is only meaningful for read-write handles; read-only handles have nothing to finalize, so the call is rejected. This protects finished backups from lifecycle mutations.","triggerScenarios":"Calling EndBackup on a GCSBackupHandle created with readOnly=true (opened for restore, listing, or verification).","commonSituations":"Code with a generic finally/cleanup block that calls EndBackup for every handle regardless of mode; error paths in restore code that mirror backup-creation cleanup logic.","solutions":["Only call EndBackup on handles produced by the backup-creation (read-write) path","Use AbortBackup-free cleanup for read-only handles — simply close files and drop the handle; there is nothing to abort or finalize","Guard cleanup code with a readOnly check before invoking EndBackup"],"exampleFix":"// before\nbh := bs.StartBackup(ctx, dir, name, true /* readOnly */)\nerr := bh.EndBackup(ctx) // error\n// after\nbh := bs.StartBackup(ctx, dir, name, false)\nerr := bh.EndBackup(ctx) // valid for read-write handles","handlingStrategy":"validation","validationCode":"if bh.readOnly {\n    return nil // nothing to finalize on a read-only handle\n}\nerr := bh.EndBackup(ctx)","typeGuard":null,"tryCatchPattern":"if err := bh.EndBackup(ctx); err != nil {\n    if strings.Contains(err.Error(), \"EndBackup cannot be called on read-only backup\") {\n        return nil // expected for read-only handles; skip finalization\n    }\n    return err\n}","preventionTips":["Skip EndBackup/AbortBackup in cleanup paths for read-only handles","Structure cleanup as: close files; EndBackup/Abort only if the handle was created for writing"],"tags":["go","mysqlctl","gcs","backup-storage","api-misuse"],"backgroundTag":"backup-handle-readonly-violation","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}