Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rqlite for openSUSE:Factory checked in at 2026-10-31 23:08:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rqlite (Old) and /work/SRC/openSUSE:Factory/.rqlite.new.1255 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rqlite" Sat Oct 31 23:08:55 2026 rev:61 rq:1394035 version:10.6.6 Changes: -------- --- /work/SRC/openSUSE:Factory/rqlite/rqlite.changes 2026-10-23 20:26:31.445081606 +0200 +++ /work/SRC/openSUSE:Factory/.rqlite.new.1255/rqlite.changes 2026-10-31 23:11:00.495474492 +0100 @@ -1,0 +2,15 @@ +Sat Oct 31 07:26:58 UTC 2026 - Andreas Stieger <[email protected]> + +- Update to version 10.6.6: + * Keep the local auto-backup file if the metadata rename fails + * Reject local auto-backup filenames reserved for internal use + * POST /demote and POST /promote return a JSON error body, and + 404 when the target node is not in the cluster. + * DELETE /remove returns a JSON error body. +- Includes changes from version 10.6.5: + * Guard the inter-node backup stream handler directly against a + missing backup request + * POST /leader returns a JSON error body, and 404 when the target + node is not in the cluster + +------------------------------------------------------------------- Old: ---- rqlite-10.6.4.tar.xz New: ---- rqlite-10.6.6.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rqlite.spec ++++++ --- /var/tmp/diff_new_pack.H0jLOE/_old 2026-10-31 23:11:02.757568404 +0100 +++ /var/tmp/diff_new_pack.H0jLOE/_new 2026-10-31 23:11:02.759568487 +0100 @@ -18,7 +18,7 @@ Name: rqlite -Version: 10.6.4 +Version: 10.6.6 Release: 0 Summary: Distributed relational database built on SQLite License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.H0jLOE/_old 2026-10-31 23:11:02.861572722 +0100 +++ /var/tmp/diff_new_pack.H0jLOE/_new 2026-10-31 23:11:02.869573054 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/rqlite/rqlite.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v10.6.4</param> + <param name="revision">v10.6.6</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.H0jLOE/_old 2026-10-31 23:11:02.924575338 +0100 +++ /var/tmp/diff_new_pack.H0jLOE/_new 2026-10-31 23:11:02.930575587 +0100 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rqlite/rqlite.git</param> - <param name="changesrevision">64145717224d620809174cb2235ac86ec604aac5</param> + <param name="changesrevision">ce2152610ad76b7371e459931c7663e0176092d2</param> </service> </servicedata> (No newline at EOF) ++++++ rqlite-10.6.4.tar.xz -> rqlite-10.6.6.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/CHANGELOG.md new/rqlite-10.6.6/CHANGELOG.md --- old/rqlite-10.6.4/CHANGELOG.md 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/CHANGELOG.md 2026-10-28 13:41:24.000000000 +0100 @@ -1,5 +1,18 @@ +## v10.6.6 (October 28th 2026) +### Implementation changes and bug fixes +- [PR #2987](https://github.com/rqlite/rqlite/pull/2987): Keep the local auto-backup file if the metadata rename fails. Thanks @lmtlssss +- [PR #2988](https://github.com/rqlite/rqlite/pull/2988): Reject local auto-backup filenames reserved for internal use. Thanks @lmtlssss +- [PR #2989](https://github.com/rqlite/rqlite/pull/2989): `POST /demote` and `POST /promote` return a JSON error body, and 404 when the target node is not in the cluster. +- [PR #2991](https://github.com/rqlite/rqlite/pull/2991): `DELETE /remove` returns a JSON error body. +- [PR #2992](https://github.com/rqlite/rqlite/pull/2992): Upgrade dependencies via `go get`. + +## v10.6.5 (October 24th 2026) +### Implementation changes and bug fixes +- [PR #2982](https://github.com/rqlite/rqlite/pull/2982): Guard the inter-node backup stream handler directly against a missing backup request. +- [PR #2984](https://github.com/rqlite/rqlite/pull/2984): `POST /leader` returns a JSON error body, and 404 when the target node is not in the cluster. + ## v10.6.4 (October 21st 2026) -This release includes [a critical security fix](https://github.com/rqlite/rqlite/security/advisories/GHSA-f4f3-px5g-gm4x). Users should upgrade. +This release includes [a critical security fix](https://github.com/rqlite/rqlite/security/advisories/GHSA-f4f3-px5g-gm4x) related to backups. An unauthorized user could download a backup if they had access to the Raft port. Upgrading strongly recommended. ### Implementation changes and bug fixes - [PR #2977](https://github.com/rqlite/rqlite/pull/2977): `GET /leader` returns a JSON error body when there is no leader. Fixes issue [#2974](https://github.com/rqlite/rqlite/issues/2974). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/auto/file/file.go new/rqlite-10.6.6/auto/file/file.go --- old/rqlite-10.6.4/auto/file/file.go 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/auto/file/file.go 2026-10-28 13:41:24.000000000 +0100 @@ -13,6 +13,11 @@ "github.com/rqlite/rqlite/v10/internal/fsutil" ) +const ( + metadataFile = "METADATA.json" + touchFile = ".touch" +) + // Config represents configuration for the file storage client. type Config struct { Dir string `json:"dir"` @@ -57,12 +62,15 @@ cleanFile != name { return nil, fmt.Errorf("invalid file parameter: %s (must be a simple filename without path separators)", name) } + if equalFoldAny(name, metadataFile, metadataFile+".tmp", touchFile) { + return nil, fmt.Errorf("invalid file parameter: %s is reserved for internal use", name) + } // Ensure the destination directory exists and is writable if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("failed to create directory %s: %w", dir, err) } - touchPath := filepath.Join(dir, ".touch") + touchPath := filepath.Join(dir, touchFile) f, err := os.OpenFile(touchPath, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, fmt.Errorf("failed to test writing to directory %s: %w", dir, err) @@ -73,7 +81,7 @@ c := &Client{ dir: dir, name: name, - metaPath: filepath.Join(dir, "METADATA.json"), + metaPath: filepath.Join(dir, metadataFile), } if opt != nil { @@ -177,8 +185,11 @@ return fmt.Errorf("failed to rename temporary file %s to %s: %w", tmpPath, finalPath, err) } + // Do not remove finalPath if the metadata rename fails. In stable-name + // mode it has already replaced the previous backup, so removing it would + // leave the existing metadata pointing at a file that no longer exists. + // Leaving it in place means a complete backup is always present. if err := fsutil.Rename(tmpMetaPath, c.metaPath); err != nil { - fsutil.Remove(finalPath) return fmt.Errorf("failed to rename temporary metadata file %s to %s: %w", tmpMetaPath, c.metaPath, err) } return nil @@ -196,6 +207,16 @@ return md.ID, nil } +// equalFoldAny returns true if s matches any of names under Unicode case-folding. +func equalFoldAny(s string, names ...string) bool { + for _, n := range names { + if strings.EqualFold(s, n) { + return true + } + } + return false +} + // timestampedPath returns a new path with the given timestamp prepended. // If path contains /, the timestamp is prepended to the last segment. func timestampedPath(path string, t time.Time) string { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/cluster/service.go new/rqlite-10.6.6/cluster/service.go --- old/rqlite-10.6.4/cluster/service.go 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/cluster/service.go 2026-10-28 13:41:24.000000000 +0100 @@ -654,30 +654,22 @@ case proto.Command_COMMAND_TYPE_BACKUP_STREAM: stats.Add(numBackupRequest, 1) - resp := &proto.CommandBackupResponse{} br := c.GetBackupRequest() if br == nil { - resp.Error = "BackupRequest is nil" - } - p, err = pb.Marshal(resp) - if err != nil { - conn.Close() - return - } - if err := writeBytesWithLength(conn, p); err != nil { + // Nothing to stream. Tell the client and end the connection. + resp := &proto.CommandBackupResponse{Error: "BackupRequest is nil"} + marshalAndWrite(conn, resp) return } - if resp.Error != "" { - // There is nothing to stream, so end the connection - // so the client sees a clean end instead of waiting. + + // Tell the client the stream is coming, then stream it. Enable + // compressed mode regardless of what the client asked for, so the + // client can detect the end of the stream and to save space on + // the wire. + if err := marshalAndWrite(conn, &proto.CommandBackupResponse{}); err != nil { return } - - // Now, start streaming the backup. Enable compressed mode - // regardless of whether the client requested it, so the client - // can easily detect the end of the stream, as well as saving - // space on the wire. br.Compress = true if _, err := s.db.Backup(context.Background(), br, conn); err != nil { s.logger.Printf("failed to stream backup: %s", err.Error()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/go.mod new/rqlite-10.6.6/go.mod --- old/rqlite-10.6.4/go.mod 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/go.mod 2026-10-28 13:41:24.000000000 +0100 @@ -3,11 +3,11 @@ go 1.27.0 require ( - github.com/aws/aws-sdk-go-v2 v1.47.3 - github.com/aws/aws-sdk-go-v2/config v1.33.9 - github.com/aws/aws-sdk-go-v2/credentials v1.20.9 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.23.14 - github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 + github.com/aws/aws-sdk-go-v2 v1.47.5 + github.com/aws/aws-sdk-go-v2/config v1.33.12 + github.com/aws/aws-sdk-go-v2/credentials v1.20.12 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.23.18 + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.6 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/raft v1.8.0 github.com/klauspost/compress v1.20.3 @@ -25,27 +25,27 @@ go.opentelemetry.io/otel/sdk v1.47.0 go.opentelemetry.io/otel/sdk/metric v1.47.0 go.opentelemetry.io/proto/otlp v1.11.1 - golang.org/x/net v0.61.0 - google.golang.org/grpc v1.84.0 + golang.org/x/net v0.62.0 + google.golang.org/grpc v1.85.0 google.golang.org/protobuf v1.36.12 ) require ( github.com/armon/go-metrics v0.7.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 // indirect - github.com/aws/smithy-go v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.25 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.8 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.39.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.44.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.51.6 // indirect + github.com/aws/smithy-go v1.28.7 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -57,8 +57,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0 // indirect - github.com/hashicorp/consul/api v1.34.5 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.33.0 // indirect + github.com/hashicorp/consul/api v1.35.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -66,7 +66,7 @@ github.com/hashicorp/go-msgpack v1.1.5 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-rootcerts v1.1.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.11.0 // indirect github.com/labstack/gommon v0.5.0 // indirect @@ -75,9 +75,9 @@ github.com/mattn/go-runewidth v0.0.31 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mkideal/expr v0.1.0 // indirect - go.etcd.io/etcd/api/v3 v3.7.2 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.7.2 // indirect - go.etcd.io/etcd/client/v3 v3.7.2 // indirect + go.etcd.io/etcd/api/v3 v3.7.3 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.7.3 // indirect + go.etcd.io/etcd/client/v3 v3.7.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/log v1.47.0 // indirect go.opentelemetry.io/otel/metric v1.47.0 // indirect @@ -89,8 +89,8 @@ golang.org/x/sys v0.49.0 // indirect golang.org/x/term v0.47.0 // indirect golang.org/x/text v0.43.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20261012220534-4341edf3f941 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261026165753-2ffab32bce7e // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261026165753-2ffab32bce7e // indirect ) replace ( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/go.sum new/rqlite-10.6.6/go.sum --- old/rqlite-10.6.4/go.sum 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/go.sum 2026-10-28 13:41:24.000000000 +0100 @@ -3,44 +3,44 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= -github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= -github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= -github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= -github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= -github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.23.14 h1:nX4dV32e7e2GSg60GojffG61w4tvYxVpvEGWfSlcImo= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.23.14/go.mod h1:dNw78YbA0ylhEVJ1h84fPdtIiWkVTaszT7oHkE/cq5E= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= -github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= -github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= -github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= -github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= -github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= -github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= -github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= -github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= -github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.47.5 h1:5yA+A+BiXbDq1ZLbYwPViwhEGDMdR0MEhL8Jh2LnLS4= +github.com/aws/aws-sdk-go-v2 v1.47.5/go.mod h1:iNIkogrRvFwm+1OQIUOP2lPBtlXZboOIUWiCa+pHEMw= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.25 h1:MW0Iql1SOTgQPj8mCxrU/eH/K/AIDWi08ItOsXaWRLU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.25/go.mod h1:+ucVYmHYB3JqVueY0E1RHahdXAnKQbIHqybSbvOUbOM= +github.com/aws/aws-sdk-go-v2/config v1.33.12 h1:IV1YARaXahwCqUIyP8IlWsP85Lil+5ozTozDQXPumRk= +github.com/aws/aws-sdk-go-v2/config v1.33.12/go.mod h1:h51R06bzMVwNBRZOX5ylHhNDnkB/azZeq/LvxM6q2pQ= +github.com/aws/aws-sdk-go-v2/credentials v1.20.12 h1:6P7Q7yH/GIwHg6MLKv3ySsBqaCoE1z+C0OTzYMenhEY= +github.com/aws/aws-sdk-go-v2/credentials v1.20.12/go.mod h1:Kw/SaoW/wc7IqJErqXJIzKo8b7Aelxfbuc6XlbrRnsc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.5 h1:iXjy2SFhGYNB1jWZBhQqq8HbhsbPF23w+/zbMvPXsTg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.5/go.mod h1:k0RUiBnyXU0AXRJ9tJmY2jQke6XUIX2PNVnWtvgaPtc= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.23.18 h1:cNToRnziFY/DKoIUg9Vw350yx0sFfaN1FCJ+xVuF53Y= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.23.18/go.mod h1:FQWrFCsxaXWlLBYB3YQOBx1zbfDNRrrVspVnnsTZ8Z0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.8 h1:gzIWc3DTPfPbWKrQnmL6BV6WD7NPD1htxzgdk/bQ/Z4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.8/go.mod h1:vvUA5cDAZWGo4R14FZPZfXfJCmgsafOWEq78Kg0D+ug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.8 h1:PWv+vG8aCLS8qd+HPdYhK7Vees90aKHWFFQ1AGYD/8E= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.8/go.mod h1:xxPZEpflYHJA12366ZGyq66z9dHPCoeCIu99Jck+MPw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.8 h1:WOp5De9iWdEHDPh2Kb0tBP3vGBm/dVAuRGh4a4tqduo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.8/go.mod h1:z8W8YgfhbWl62XqcTtK+CxPrE+iHGRVUOtrrbIOp7ZY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.23 h1:MCIR6iZaCKO4UE0hUvoWWLI6JVjppopV64JfAJaMjng= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.23/go.mod h1:pphWFS+z0MmIPivj8c/6bATd1Z+Av7Ust75DPdEFsPA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.9 h1:GACgECI422Xt5n4Pojr3O0aocPv8lfH4PJe2Aon5Wv8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.9/go.mod h1:lPfnRkHpn1qWZJLbVEw10rIqSuCee1pNQTyGkUjAYh8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.8 h1:+PMpWlJYOn2tm7sm6OZ/ZLd4jStl17T7wjNapd1Rczs= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.8/go.mod h1:cnG3xZLGQL4c/bt8pjdzlFtZsW7epBBuNS04a5xMjdo= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.8 h1:RS1Wt+K0r4X0ci9Vf4JYTUfHzsQihYA2LgGJeEneKpk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.8/go.mod h1:adMnTy5kyoUW86jhXnGSVc/mvMQI4GZd1qYWMeaS8M8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.6 h1:to7+Vr3rLkpYRwVKJ5QRriI0eE306vrVN/6TnVHgxvY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.6/go.mod h1:Jc89ASa2k7HMAskj3bSnt8bf+k+p2IApzkoXnEjkgUw= +github.com/aws/aws-sdk-go-v2/service/signin v1.12.0 h1:3vu/eUqh0rjh7Zy4N4YKcpZX4Aud+zinB3VsLhhlQFw= +github.com/aws/aws-sdk-go-v2/service/signin v1.12.0/go.mod h1:wxAk0D2oVBSjuXEIzvcKxTZfM90zticdAbh/VD42L0Y= +github.com/aws/aws-sdk-go-v2/service/sso v1.39.0 h1:kIEpD5dUry7VET3Ybl7zj7PPHRB4lb52G9iTz+a1Tqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.39.0/go.mod h1:UGsEYgWz4Gi+46vDQ5zkFLqUgJziyOE2SXjZjgmfmHc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.44.0 h1:WfFev+HgTodggcNon5HieiLxxwWwb8Br8R2+nDkixvk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.44.0/go.mod h1:gJSdIY6YUzDvqQLcMtPVZgBRHrFwzHryvpE8Igzbxps= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.6 h1:BVjKrtRUtSYKztBCCfE1oAB+d3ViKcW2cKZfserOqMQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.6/go.mod h1:eSukFLeLkPMm+1OxDR+F0joauZdyBkF47Ow7MqjZiJw= +github.com/aws/smithy-go v1.28.7 h1:h/n1SHrwVtaDYLz8mR1/Kq4Xh4RaFbFW7bOYckg9mfE= +github.com/aws/smithy-go v1.28.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -92,12 +92,12 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0 h1:QMILzF8BA7xftE4zVORkRrlv08m12St3csK4RsyRgRs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0/go.mod h1:+ZQ6iUL8+ATyxGDQNuRKpNwJFdGDUi0e7E1QL9MvI7U= -github.com/hashicorp/consul/api v1.34.5 h1:QpMhHZyfYsOsIu5n5QA7TQTLabM4OQJEbKi3pXXnw7U= -github.com/hashicorp/consul/api v1.34.5/go.mod h1:OrXEufkaxFy1pMIRHFrn3JkuircxMhA4BHHpbR8k+5U= -github.com/hashicorp/consul/sdk v0.18.2 h1:wMFx4OkUPg8un6kimUmzADVBsuRqUdNRtJ0KREGs7vM= -github.com/hashicorp/consul/sdk v0.18.2/go.mod h1:2V4Z2YguOFZelOtkQs3UnIrkCXDQ6iL3P4B6EtSqoQY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.33.0 h1:5T5Ns3kMpWr+z3s5ZA0e7ljVyWNVNxK76sHPAxi5D1k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.33.0/go.mod h1:oWNYw09cH8wl8fFgC5hKlBLxR4XfKy2KQpP8MJjeCcc= +github.com/hashicorp/consul/api v1.35.0 h1:4ULHUF0DDHcDqQE48KpdbV7PIIQDua3UaJ/bEcUiTE0= +github.com/hashicorp/consul/api v1.35.0/go.mod h1:RwV43v4y1mHVnJY2V9m89rR/0mDF9QL6q7+G0Rkma/I= +github.com/hashicorp/consul/sdk v0.19.0 h1:zucuiWFKpggzzrx9MPHCpei74gnrLoHT/TGP2NmegHc= +github.com/hashicorp/consul/sdk v0.19.0/go.mod h1:Xak7xFgsTYKR6j523cZDqCgZy0g/JvKMNs9MhX7MUM4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -122,8 +122,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-rootcerts v1.1.0 h1:c5kkPHzmvHrgKn7I1goudi6h62PsRTZt2JddV18QAtA= +github.com/hashicorp/go-rootcerts v1.1.0/go.mod h1:HJQ+oBQQZMdSLaLBrdKWoQzKCc3vwdgySXfrBgUTgt8= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -233,12 +233,12 @@ go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= -go.etcd.io/etcd/api/v3 v3.7.2 h1:xgt/6el1LsPWWYNLkhMAK4tZm6dF+1sCqDecpE5gdbk= -go.etcd.io/etcd/api/v3 v3.7.2/go.mod h1:RoRCBRt9BfBff1pIGZLUVMiz7wu3bY+b2qLysGu1HY4= -go.etcd.io/etcd/client/pkg/v3 v3.7.2 h1:SVtlR7tiSVAYOQ4nWPIyFXb4RMgEcnzeAG9RQ8MoNDU= -go.etcd.io/etcd/client/pkg/v3 v3.7.2/go.mod h1:HsSux/B3ahgyw/D5+d4YbZqicOi0mEbuxm6lIUdjAoI= -go.etcd.io/etcd/client/v3 v3.7.2 h1:Z66GqDQDI7zPDfVSsIBqGSK4mJYLtv8ESwXa4mPf+wY= -go.etcd.io/etcd/client/v3 v3.7.2/go.mod h1:x03t1qMs4tGZirCDJlMuzPBJdQffXJImIyEjLhNBCsY= +go.etcd.io/etcd/api/v3 v3.7.3 h1:2tumepXe+wi7Wab64uUO71K7ISQyUWLGC1DJbbc59sY= +go.etcd.io/etcd/api/v3 v3.7.3/go.mod h1:HOR9Qn4gb/1/hVAmYprmlLpvTe6ZgfgNS9Z/+Ty65po= +go.etcd.io/etcd/client/pkg/v3 v3.7.3 h1:5zMwNu3ycrzCeA5NwOfQbxFRhqq974chq1DrYMua4QI= +go.etcd.io/etcd/client/pkg/v3 v3.7.3/go.mod h1:/OhzqYrF0U7d1NOk55VMyUQGbYD1WoIe4bJwxb47wOk= +go.etcd.io/etcd/client/v3 v3.7.3 h1:UrXcetgkuSEocaBlQlulQRJlXLEnUAHuBRbHh0lue1Q= +go.etcd.io/etcd/client/v3 v3.7.3/go.mod h1:DcPxpqcpV8WIaRi/2waPCx0/Uq8rQUweDJaDYZk1gEE= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/runtime v0.72.0 h1:rv8t3kKjyC2/9D99L1MZt4tJSZO4aYUmHus7L//iKKg= @@ -282,8 +282,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= -golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -318,12 +318,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20261012220534-4341edf3f941 h1:DS/w3N8rsfrwxwgGSj6WhMsPo7aNQGmDx74J81zd0NQ= -google.golang.org/genproto/googleapis/api v0.0.0-20261012220534-4341edf3f941/go.mod h1:AuGAdi9KnXss7xbLxIKhCvk+Ro/SxpET/iS45DNYN+M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 h1:QG6hS4zFplRZvOQedzei4t2pT/zkQc3VR1u+S9/rcAw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= -google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= -google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/genproto/googleapis/api v0.0.0-20261026165753-2ffab32bce7e h1:e789sLZjeTdc02ZCQGTSzHOu0fR73+dwPQVKUp67Z88= +google.golang.org/genproto/googleapis/api v0.0.0-20261026165753-2ffab32bce7e/go.mod h1:vHV3wJM9WZyfV7GyB+OQGq5dWVrFwCDxapPK4WlRD88= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261026165753-2ffab32bce7e h1:w3fy/G8OrJHYexn+u5y7hFyc8YRRIr7DCevtIOwAcGY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261026165753-2ffab32bce7e/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.85.0 h1:BPBCfuVRQ1qvBhKDOq9uvQllMvO59rKsYhXpp7Dd8vw= +google.golang.org/grpc v1.85.0/go.mod h1:0rQrOD58yjal/0EaVbGBANindixsCZNQPDV3VKtaKuQ= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/http/service.go new/rqlite-10.6.6/http/service.go --- old/rqlite-10.6.4/http/service.go 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/http/service.go 2026-10-28 13:41:24.000000000 +0100 @@ -550,7 +550,14 @@ } // handleRemove handles cluster-remove requests. +// +// Unlike the other node-management endpoints, removing a node which is not +// in the cluster is not reported as an error. Raft treats such a removal as +// a no-op and this endpoint has always returned 200 for it, so clients and +// the shutdown self-removal path depend on that behavior. func (s *Service) handleRemove(w http.ResponseWriter, r *http.Request, qp QueryParams) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if !s.CheckRequestPerm(r, auth.PermRemove) { w.WriteHeader(http.StatusUnauthorized) return @@ -577,14 +584,17 @@ } if errors.Is(err, proxy.ErrLeaderNotFound) { stats.Add(numLeaderNotFound, 1) - http.Error(w, proxy.ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) + w.WriteHeader(http.StatusServiceUnavailable) + s.writeResponse(w, qp, &Response{Error: proxy.ErrLeaderNotFound.Error()}) return } if errors.Is(err, proxy.ErrUnauthorized) { - http.Error(w, "remote remove node not authorized", http.StatusUnauthorized) + w.WriteHeader(http.StatusUnauthorized) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } - http.Error(w, err.Error(), http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } w.Header().Set(ServedByHTTPHeader, addr) @@ -592,6 +602,8 @@ // handleDemote handles requests to demote a voting node to a non-voter. func (s *Service) handleDemote(w http.ResponseWriter, r *http.Request, qp QueryParams) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if !s.CheckRequestPerm(r, auth.PermDemote) { w.WriteHeader(http.StatusUnauthorized) return @@ -618,14 +630,21 @@ } if errors.Is(err, proxy.ErrLeaderNotFound) { stats.Add(numLeaderNotFound, 1) - http.Error(w, proxy.ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) + w.WriteHeader(http.StatusServiceUnavailable) + s.writeResponse(w, qp, &Response{Error: proxy.ErrLeaderNotFound.Error()}) return } if errors.Is(err, proxy.ErrUnauthorized) { - http.Error(w, "remote demote node not authorized", http.StatusUnauthorized) + w.WriteHeader(http.StatusUnauthorized) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } - http.Error(w, err.Error(), http.StatusInternalServerError) + statusCode := http.StatusInternalServerError + if errors.Is(err, proxy.ErrNodeNotFound) { + statusCode = http.StatusNotFound + } + w.WriteHeader(statusCode) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } w.Header().Set(ServedByHTTPHeader, addr) @@ -633,6 +652,8 @@ // handlePromote handles requests to promote a non-voting node to a voter. func (s *Service) handlePromote(w http.ResponseWriter, r *http.Request, qp QueryParams) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if !s.CheckRequestPerm(r, auth.PermPromote) { w.WriteHeader(http.StatusUnauthorized) return @@ -659,14 +680,21 @@ } if errors.Is(err, proxy.ErrLeaderNotFound) { stats.Add(numLeaderNotFound, 1) - http.Error(w, proxy.ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) + w.WriteHeader(http.StatusServiceUnavailable) + s.writeResponse(w, qp, &Response{Error: proxy.ErrLeaderNotFound.Error()}) return } if errors.Is(err, proxy.ErrUnauthorized) { - http.Error(w, "remote promote node not authorized", http.StatusUnauthorized) + w.WriteHeader(http.StatusUnauthorized) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } - http.Error(w, err.Error(), http.StatusInternalServerError) + statusCode := http.StatusInternalServerError + if errors.Is(err, proxy.ErrNodeNotFound) { + statusCode = http.StatusNotFound + } + w.WriteHeader(statusCode) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } w.Header().Set(ServedByHTTPHeader, addr) @@ -1272,19 +1300,24 @@ } if errors.Is(err, proxy.ErrLeaderNotFound) { stats.Add(numLeaderNotFound, 1) - http.Error(w, proxy.ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) + w.WriteHeader(http.StatusServiceUnavailable) + s.writeResponse(w, qp, &Response{Error: proxy.ErrLeaderNotFound.Error()}) return } if errors.Is(err, proxy.ErrUnauthorized) { - http.Error(w, "remote stepdown not authorized", http.StatusUnauthorized) + w.WriteHeader(http.StatusUnauthorized) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } statusCode := http.StatusInternalServerError - if err == store.ErrNotOpen { + if errors.Is(err, store.ErrNotOpen) { statusCode = http.StatusServiceUnavailable + } else if errors.Is(err, proxy.ErrNodeNotFound) { + statusCode = http.StatusNotFound } - http.Error(w, fmt.Sprintf("stepdown: %s", err.Error()), statusCode) + w.WriteHeader(statusCode) + s.writeResponse(w, qp, &Response{Error: err.Error()}) return } w.Header().Set(ServedByHTTPHeader, addr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/http/service_test.go new/rqlite-10.6.6/http/service_test.go --- old/rqlite-10.6.4/http/service_test.go 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/http/service_test.go 2026-10-28 13:41:24.000000000 +0100 @@ -1913,8 +1913,188 @@ t.Fatalf("expected 500, got %d", rr.Code) } - if !strings.Contains(rr.Body.String(), "stepdown failed") { - t.Fatalf("expected error message to contain 'stepdown failed', got %s", rr.Body.String()) + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != "stepdown failed" { + t.Fatalf("expected error 'stepdown failed', got %q", resp.Error) + } +} + +func Test_Leader_POST_NodeNotFound(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + stepdownFn: func(wait bool, id string) error { + return store.ErrNodeNotFound + }, + } + cluster := &mockClusterService{apiAddr: "http://127.0.0.1:4001"} + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("POST", "/leader", strings.NewReader(`{"id": "nonexistent"}`)) + if err != nil { + t.Fatalf("failed to create POST request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rr.Code) + } + + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != store.ErrNodeNotFound.Error() { + t.Fatalf("expected error %q, got %q", store.ErrNodeNotFound.Error(), resp.Error) + } +} + +func Test_Leader_POST_NodeNotFound_Forwarded(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + stepdownFn: func(wait bool, id string) error { + return store.ErrNotLeader + }, + } + cluster := &mockClusterService{ + apiAddr: "http://127.0.0.1:4001", + stepdownFn: func(sr *command.StepdownRequest, nodeAddr string, t time.Duration) error { + // The cluster client rebuilds the remote error from its message. + return errors.New(store.ErrNodeNotFound.Error()) + }, + } + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("POST", "/leader", strings.NewReader(`{"id": "nonexistent"}`)) + if err != nil { + t.Fatalf("failed to create POST request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rr.Code) + } + + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != store.ErrNodeNotFound.Error() { + t.Fatalf("expected error %q, got %q", store.ErrNodeNotFound.Error(), resp.Error) + } +} + +func Test_Remove_DELETE(t *testing.T) { + removedID := "" + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + removeFn: func(rn *command.RemoveNodeRequest) error { + removedID = rn.Id + return nil + }, + } + cluster := &mockClusterService{apiAddr: "http://127.0.0.1:4001"} + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("DELETE", "/remove", strings.NewReader(`{"id": "node1"}`)) + if err != nil { + t.Fatalf("failed to create DELETE request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + if removedID != "node1" { + t.Fatalf("expected node1 to be removed, got %q", removedID) + } +} + +func Test_Remove_DELETE_StoreError(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + removeFn: func(rn *command.RemoveNodeRequest) error { + return errors.New("remove failed") + }, + } + cluster := &mockClusterService{apiAddr: "http://127.0.0.1:4001"} + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("DELETE", "/remove", strings.NewReader(`{"id": "node1"}`)) + if err != nil { + t.Fatalf("failed to create DELETE request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != "remove failed" { + t.Fatalf("expected error 'remove failed', got %q", resp.Error) + } +} + +func Test_Remove_DELETE_ForwardError(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + removeFn: func(rn *command.RemoveNodeRequest) error { + return store.ErrNotLeader + }, + } + cluster := &mockClusterService{ + apiAddr: "http://127.0.0.1:4001", + removeNodeFn: func(rn *command.RemoveNodeRequest, nodeAddr string, t time.Duration) error { + return errors.New("remove failed") + }, + } + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("DELETE", "/remove", strings.NewReader(`{"id": "node1"}`)) + if err != nil { + t.Fatalf("failed to create DELETE request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != "remove failed" { + t.Fatalf("expected error 'remove failed', got %q", resp.Error) } } @@ -1996,8 +2176,48 @@ if rr.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rr.Code) } - if !strings.Contains(rr.Body.String(), "demote failed") { - t.Fatalf("expected error message to contain 'demote failed', got %s", rr.Body.String()) + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != "demote failed" { + t.Fatalf("expected error 'demote failed', got %q", resp.Error) + } +} + +func Test_Demote_POST_NodeNotFound(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + demoteFn: func(dn *command.DemoteNodeRequest) error { + return store.ErrNodeNotFound + }, + } + cluster := &mockClusterService{apiAddr: "http://127.0.0.1:4001"} + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("POST", "/demote", strings.NewReader(`{"id": "nonexistent"}`)) + if err != nil { + t.Fatalf("failed to create POST request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rr.Code) + } + + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != store.ErrNodeNotFound.Error() { + t.Fatalf("expected error %q, got %q", store.ErrNodeNotFound.Error(), resp.Error) } } @@ -2067,8 +2287,54 @@ if rr.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rr.Code) } - if !strings.Contains(rr.Body.String(), "demote failed") { - t.Fatalf("expected error message to contain 'demote failed', got %s", rr.Body.String()) + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != "demote failed" { + t.Fatalf("expected error 'demote failed', got %q", resp.Error) + } +} + +func Test_Demote_POST_NodeNotFound_Forwarded(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + demoteFn: func(dn *command.DemoteNodeRequest) error { + return store.ErrNotLeader + }, + } + cluster := &mockClusterService{ + apiAddr: "http://127.0.0.1:4001", + demoteNodeFn: func(dn *command.DemoteNodeRequest, nodeAddr string, t time.Duration) error { + // The cluster client rebuilds the remote error from its message. + return errors.New(store.ErrNodeNotFound.Error()) + }, + } + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("POST", "/demote", strings.NewReader(`{"id": "nonexistent"}`)) + if err != nil { + t.Fatalf("failed to create POST request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rr.Code) + } + + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != store.ErrNodeNotFound.Error() { + t.Fatalf("expected error %q, got %q", store.ErrNodeNotFound.Error(), resp.Error) } } @@ -2180,8 +2446,48 @@ if rr.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rr.Code) } - if !strings.Contains(rr.Body.String(), "promote failed") { - t.Fatalf("expected error message to contain 'promote failed', got %s", rr.Body.String()) + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != "promote failed" { + t.Fatalf("expected error 'promote failed', got %q", resp.Error) + } +} + +func Test_Promote_POST_NodeNotFound(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + promoteFn: func(dn *command.PromoteNodeRequest) error { + return store.ErrNodeNotFound + }, + } + cluster := &mockClusterService{apiAddr: "http://127.0.0.1:4001"} + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("POST", "/promote", strings.NewReader(`{"id": "nonexistent"}`)) + if err != nil { + t.Fatalf("failed to create POST request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rr.Code) + } + + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != store.ErrNodeNotFound.Error() { + t.Fatalf("expected error %q, got %q", store.ErrNodeNotFound.Error(), resp.Error) } } @@ -2251,8 +2557,54 @@ if rr.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rr.Code) } - if !strings.Contains(rr.Body.String(), "promote failed") { - t.Fatalf("expected error message to contain 'promote failed', got %s", rr.Body.String()) + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != "promote failed" { + t.Fatalf("expected error 'promote failed', got %q", resp.Error) + } +} + +func Test_Promote_POST_NodeNotFound_Forwarded(t *testing.T) { + st := &MockStore{ + leaderAddr: "127.0.0.1:8001", + promoteFn: func(dn *command.PromoteNodeRequest) error { + return store.ErrNotLeader + }, + } + cluster := &mockClusterService{ + apiAddr: "http://127.0.0.1:4001", + promoteNodeFn: func(dn *command.PromoteNodeRequest, nodeAddr string, t time.Duration) error { + // The cluster client rebuilds the remote error from its message. + return errors.New(store.ErrNodeNotFound.Error()) + }, + } + cred := &mockCredentialStore{HasPermOK: true} + + s := New("127.0.0.1:4001", st, cluster, proxy.New(st, cluster), cred) + + req, err := http.NewRequest("POST", "/promote", strings.NewReader(`{"id": "nonexistent"}`)) + if err != nil { + t.Fatalf("failed to create POST request: %s", err.Error()) + } + rr := httptest.NewRecorder() + s.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rr.Code) + } + + var resp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got %q: %s", rr.Body.String(), err) + } + if resp.Error != store.ErrNodeNotFound.Error() { + t.Fatalf("expected error %q, got %q", store.ErrNodeNotFound.Error(), resp.Error) } } @@ -2788,6 +3140,7 @@ readFromFn func(r io.Reader) (int64, error) committedFn func(timeout time.Duration) (uint64, error) stepdownFn func(wait bool, id string) error + removeFn func(rn *command.RemoveNodeRequest) error demoteFn func(dn *command.DemoteNodeRequest) error promoteFn func(pn *command.PromoteNodeRequest) error leaderAddr string @@ -2825,6 +3178,9 @@ } func (m *MockStore) Remove(ctx context.Context, rn *command.RemoveNodeRequest) error { + if m.removeFn != nil { + return m.removeFn(rn) + } return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/proxy/proxy.go new/rqlite-10.6.6/proxy/proxy.go --- old/rqlite-10.6.4/proxy/proxy.go 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/proxy/proxy.go 2026-10-28 13:41:24.000000000 +0100 @@ -26,6 +26,10 @@ // It is an alias for store.ErrLeaderNotFound. ErrLeaderNotFound = store.ErrLeaderNotFound + // ErrNodeNotFound is returned when a node referenced by ID is not + // found in the cluster. + ErrNodeNotFound = store.ErrNodeNotFound + // ErrUnauthorized is returned when the remote node rejects credentials. ErrUnauthorized = errors.New("unauthorized") ) @@ -269,7 +273,7 @@ } err = p.cluster.DemoteNode(ctx, dn, addr, creds, timeout) if err != nil { - return "", wrapIfUnauthorized(err) + return "", wrapRemoteNodeError(err) } stats.Add(numRemoteDemoteNode, 1) return addr, nil @@ -294,7 +298,7 @@ } err = p.cluster.PromoteNode(ctx, pn, addr, creds, timeout) if err != nil { - return "", wrapIfUnauthorized(err) + return "", wrapRemoteNodeError(err) } stats.Add(numRemotePromoteNode, 1) return addr, nil @@ -326,7 +330,7 @@ } err = p.cluster.Stepdown(ctx, sr, addr, creds, timeout) if err != nil { - return "", wrapIfUnauthorized(err) + return "", wrapRemoteNodeError(err) } stats.Add(numRemoteStepdowns, 1) return addr, nil @@ -347,6 +351,19 @@ return addr, nil } +// wrapRemoteNodeError restores the sentinel for an error returned by a +// remote node-management operation. The remote node's error arrives as a +// bare string, so it is matched by message. +func wrapRemoteNodeError(err error) error { + if err == nil { + return nil + } + if err.Error() == ErrNodeNotFound.Error() { + return ErrNodeNotFound + } + return wrapIfUnauthorized(err) +} + // wrapIfUnauthorized wraps an error as ErrUnauthorized if the error // message is "unauthorized", allowing callers to use errors.Is(). func wrapIfUnauthorized(err error) error { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/proxy/proxy_test.go new/rqlite-10.6.6/proxy/proxy_test.go --- old/rqlite-10.6.4/proxy/proxy_test.go 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/proxy/proxy_test.go 2026-10-28 13:41:24.000000000 +0100 @@ -794,6 +794,30 @@ } } +func Test_Demote_NotLeader_ForwardNodeNotFound(t *testing.T) { + t.Parallel() + s := &mockStore{ + demoteFn: func(ctx context.Context, dn *proto.DemoteNodeRequest) error { + return store.ErrNotLeader + }, + leaderAddrFn: func() (string, error) { + return "leader:4002", nil + }, + } + c := &mockCluster{ + demoteNodeFn: func(ctx context.Context, dn *proto.DemoteNodeRequest, nodeAddr string, creds *clstrPB.Credentials, timeout time.Duration) error { + // The cluster client rebuilds the remote error from its message. + return errors.New(store.ErrNodeNotFound.Error()) + }, + } + p := newTestProxy(s, c) + + _, err := p.Demote(context.Background(), &proto.DemoteNodeRequest{Id: "nonexistent"}, nil, time.Second, false) + if !errors.Is(err, ErrNodeNotFound) { + t.Fatalf("expected ErrNodeNotFound, got %v", err) + } +} + func Test_Promote_LocalSuccess(t *testing.T) { t.Parallel() called := false @@ -929,6 +953,30 @@ } } +func Test_Promote_NotLeader_ForwardNodeNotFound(t *testing.T) { + t.Parallel() + s := &mockStore{ + promoteFn: func(ctx context.Context, dn *proto.PromoteNodeRequest) error { + return store.ErrNotLeader + }, + leaderAddrFn: func() (string, error) { + return "leader:4002", nil + }, + } + c := &mockCluster{ + promoteNodeFn: func(ctx context.Context, dn *proto.PromoteNodeRequest, nodeAddr string, creds *clstrPB.Credentials, timeout time.Duration) error { + // The cluster client rebuilds the remote error from its message. + return errors.New(store.ErrNodeNotFound.Error()) + }, + } + p := newTestProxy(s, c) + + _, err := p.Promote(context.Background(), &proto.PromoteNodeRequest{Id: "nonexistent"}, nil, time.Second, false) + if !errors.Is(err, ErrNodeNotFound) { + t.Fatalf("expected ErrNodeNotFound, got %v", err) + } +} + func Test_Stepdown_LocalSuccess(t *testing.T) { t.Parallel() s := &mockStore{} @@ -1016,6 +1064,30 @@ } } +func Test_Stepdown_NodeNotFound_Forward(t *testing.T) { + t.Parallel() + s := &mockStore{ + stepdownFn: func(wait bool, id string) error { + return store.ErrNotLeader + }, + leaderAddrFn: func() (string, error) { + return "leader:4002", nil + }, + } + c := &mockCluster{ + stepdownFn: func(ctx context.Context, sr *proto.StepdownRequest, nodeAddr string, creds *clstrPB.Credentials, timeout time.Duration) error { + // The cluster client rebuilds the remote error from its message. + return errors.New(store.ErrNodeNotFound.Error()) + }, + } + p := newTestProxy(s, c) + + _, err := p.Stepdown(context.Background(), true, "nonexistent", nil, time.Second, false) + if !errors.Is(err, ErrNodeNotFound) { + t.Fatalf("expected ErrNodeNotFound, got %v", err) + } +} + func Test_LeaderAddrError(t *testing.T) { t.Parallel() leaderErr := errors.New("raft error") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.6.4/store/store_cdc_test.go new/rqlite-10.6.6/store/store_cdc_test.go --- old/rqlite-10.6.4/store/store_cdc_test.go 2026-10-22 04:39:23.000000000 +0200 +++ new/rqlite-10.6.6/store/store_cdc_test.go 2026-10-28 13:41:24.000000000 +0100 @@ -8,6 +8,7 @@ "time" "github.com/rqlite/rqlite/v10/command/proto" + "github.com/rqlite/rqlite/v10/internal/fsutil" "github.com/rqlite/rqlite/v10/internal/random" ) @@ -342,3 +343,80 @@ } return s, ly } + +// Test_StoreCDC_Events_AfterSnapshotRestore tests that CDC events are still +// emitted after the node restores its database from a Raft snapshot. The +// restore swaps in a new SQLite file, so the CDC hooks must survive the reopen. +func Test_StoreCDC_Events_AfterSnapshotRestore(t *testing.T) { + s, ln := mustNewStoreCDC(t) + defer ln.Close() + + if err := s.Open(); err != nil { + t.Fatalf("failed to open single-node store: %s", err.Error()) + } + if err := s.Bootstrap(NewServer(s.ID(), s.Addr(), true)); err != nil { + t.Fatalf("failed to bootstrap single-node store: %s", err.Error()) + } + defer s.Close(true) + if _, err := s.WaitForLeader(10 * time.Second); err != nil { + t.Fatalf("Error waiting for leader: %s", err) + } + + er := executeRequestFromString(`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`, false, false) + if _, _, err := s.Execute(context.Background(), er); err != nil { + t.Fatalf("failed to execute CREATE TABLE on single node: %s", err.Error()) + } + er = executeRequestFromString(`INSERT INTO foo(id, name) VALUES(101, "fiona")`, false, false) + if _, _, err := s.Execute(context.Background(), er); err != nil { + t.Fatalf("failed to execute INSERT on single node: %s", err.Error()) + } + mustReceiveCDCInsert(t, s, 101) + + // Snapshot, then restart without the clean-snapshot marker so the node + // cannot fast-restart on the existing SQLite file and must restore from + // the Raft snapshot. + if err := s.Snapshot(0); err != nil { + t.Fatalf("failed to take snapshot: %s", err.Error()) + } + if err := s.Close(true); err != nil { + t.Fatalf("failed to close single-node store: %s", err.Error()) + } + if err := fsutil.Remove(s.cleanSnapshotPath); err != nil { + t.Fatalf("failed to remove clean snapshot marker: %s", err.Error()) + } + if err := s.Open(); err != nil { + t.Fatalf("failed to reopen single-node store: %s", err.Error()) + } + defer s.Close(true) + if _, err := s.WaitForLeader(10 * time.Second); err != nil { + t.Fatalf("Error waiting for leader: %s", err) + } + if s.numSnapshotsStart.Load() != 1 { + t.Fatalf("expected snapshot start count to be 1, got %d", s.numSnapshotsStart.Load()) + } + + er = executeRequestFromString(`INSERT INTO foo(id, name) VALUES(102, "declan")`, false, false) + if _, _, err := s.Execute(context.Background(), er); err != nil { + t.Fatalf("failed to execute INSERT after restore: %s", err.Error()) + } + mustReceiveCDCInsert(t, s, 102) +} + +// mustReceiveCDCInsert waits for a single CDC INSERT event on table foo with +// the given row ID, failing the test if it does not arrive. +func mustReceiveCDCInsert(t *testing.T, s *Store, rowID int64) { + t.Helper() + select { + case events := <-s.CDCEventsC(): + if events == nil || len(events.Events) != 1 { + t.Fatalf("expected 1 CDC event, got %v", events) + } + ev := events.Events[0] + if ev.Table != "foo" || ev.Op != proto.CDCEvent_INSERT || ev.NewRowId != rowID { + t.Fatalf("expected INSERT on foo with row ID %d, got %s on %s with row ID %d", + rowID, ev.Op, ev.Table, ev.NewRowId) + } + case <-time.After(5 * time.Second): + t.Fatalf("timeout waiting for CDC INSERT event with row ID %d", rowID) + } +} ++++++ vendor.tar.xz ++++++ ++++ 25036 lines of diff (skipped)
