Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package rqlite for openSUSE:Factory checked 
in at 2025-03-17 22:17:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rqlite (Old)
 and      /work/SRC/openSUSE:Factory/.rqlite.new.19136 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rqlite"

Mon Mar 17 22:17:28 2025 rev:11 rq:1253514 version:8.36.14

Changes:
--------
--- /work/SRC/openSUSE:Factory/rqlite/rqlite.changes    2025-03-13 
15:07:42.974022297 +0100
+++ /work/SRC/openSUSE:Factory/.rqlite.new.19136/rqlite.changes 2025-03-17 
22:21:24.324033725 +0100
@@ -1,0 +2,7 @@
+Sun Mar 16 13:46:03 UTC 2025 - Andreas Stieger <andreas.stie...@gmx.de>
+
+- Update to version 8.36.14:
+  * Support SQL-format loading via Follower
+  * Minor refactor of HTTP credential handling
+
+-------------------------------------------------------------------

Old:
----
  rqlite-8.36.13.tar.xz

New:
----
  rqlite-8.36.14.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rqlite.spec ++++++
--- /var/tmp/diff_new_pack.1cCsku/_old  2025-03-17 22:21:24.860056115 +0100
+++ /var/tmp/diff_new_pack.1cCsku/_new  2025-03-17 22:21:24.864056283 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           rqlite
-Version:        8.36.13
+Version:        8.36.14
 Release:        0
 Summary:        Distributed relational database built on SQLite
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.1cCsku/_old  2025-03-17 22:21:24.900057786 +0100
+++ /var/tmp/diff_new_pack.1cCsku/_new  2025-03-17 22:21:24.904057953 +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">v8.36.13</param>
+    <param name="revision">v8.36.14</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="changesgenerate">enable</param>
     <param name="versionrewrite-pattern">v(.*)</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.1cCsku/_old  2025-03-17 22:21:24.924058789 +0100
+++ /var/tmp/diff_new_pack.1cCsku/_new  2025-03-17 22:21:24.928058955 +0100
@@ -1,7 +1,7 @@
 <servicedata>
   <service name="tar_scm">
     <param name="url">https://github.com/rqlite/rqlite.git</param>
-    <param 
name="changesrevision">15827cba077aeef4815f6a7ee04b8427e16c85f1</param>
+    <param 
name="changesrevision">000abd80ae237adc7e538e2e800d80a099861f7e</param>
   </service>
 </servicedata>
 (No newline at EOF)

++++++ rqlite-8.36.13.tar.xz -> rqlite-8.36.14.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/CHANGELOG.md 
new/rqlite-8.36.14/CHANGELOG.md
--- old/rqlite-8.36.13/CHANGELOG.md     2025-03-12 04:47:36.000000000 +0100
+++ new/rqlite-8.36.14/CHANGELOG.md     2025-03-15 16:18:40.000000000 +0100
@@ -1,3 +1,9 @@
+## v8.36.14 (March 15th 2025)
+### Implementation changes and bug fixes
+- [PR #2056](https://github.com/rqlite/rqlite/pull/2056): Minor refactoring of 
HTTP credentials handling.
+- [PR #2054](https://github.com/rqlite/rqlite/pull/2054): Bump 
golang.org/x/net from 0.35.0 to 0.36.0.
+- [PR #2055](https://github.com/rqlite/rqlite/pull/2055): Support SQL-format 
loading via Follower. Fixes issue 
[#2053](https://github.com/rqlite/rqlite/issues/2053).
+
 ## v8.36.13 (March 11th 2025)
 ### Implementation changes and bug fixes
 - [PR #2051](https://github.com/rqlite/rqlite/pull/2051): System-level test of 
SQL format backups.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/cluster/client.go 
new/rqlite-8.36.14/cluster/client.go
--- old/rqlite-8.36.13/cluster/client.go        2025-03-12 04:47:36.000000000 
+0100
+++ new/rqlite-8.36.14/cluster/client.go        2025-03-15 16:18:40.000000000 
+0100
@@ -169,7 +169,7 @@
        return a.CommitIndex, nil
 }
 
-// Execute performs an Execute on a remote node. If username is an empty string
+// Execute performs an Execute on a remote node. If creds is nil, then
 // no credential information will be included in the Execute request to the
 // remote node.
 func (c *Client) Execute(er *command.ExecuteRequest, nodeAddr string, creds 
*proto.Credentials, timeout time.Duration, retries int) 
([]*command.ExecuteQueryResponse, error) {
@@ -198,7 +198,9 @@
        return a.Response, nil
 }
 
-// Query performs a Query on a remote node.
+// Query performs a Query on a remote node. If creds is nil, then
+// no credential information will be included in the Query request to the
+// remote node.
 func (c *Client) Query(qr *command.QueryRequest, nodeAddr string, creds 
*proto.Credentials, timeout time.Duration) ([]*command.QueryRows, error) {
        command := &proto.Command{
                Type: proto.Command_COMMAND_TYPE_QUERY,
@@ -225,7 +227,9 @@
        return a.Rows, nil
 }
 
-// Request performs an ExecuteQuery on a remote node.
+// Request performs an ExecuteQuery on a remote node. If creds is nil, then
+// no credential information will be included in the ExecuteQuery request to 
the
+// remote node.
 func (c *Client) Request(r *command.ExecuteQueryRequest, nodeAddr string, 
creds *proto.Credentials, timeout time.Duration, retries int) 
([]*command.ExecuteQueryResponse, error) {
        command := &proto.Command{
                Type: proto.Command_COMMAND_TYPE_REQUEST,
@@ -252,7 +256,9 @@
        return a.Response, nil
 }
 
-// Backup retrieves a backup from a remote node and writes to the io.Writer
+// Backup retrieves a backup from a remote node and writes to the io.Writer.
+// If creds is nil, then no credential information will be included in the
+// Backup request to the remote node.
 func (c *Client) Backup(br *command.BackupRequest, nodeAddr string, creds 
*proto.Credentials, timeout time.Duration, w io.Writer) error {
        conn, err := c.dial(nodeAddr)
        if err != nil {
@@ -305,7 +311,8 @@
        return err
 }
 
-// Load loads a SQLite file into the database.
+// Load loads a SQLite file into the database. If creds is nil, then no
+// credential information will be included in the Load request to the remote 
node.
 func (c *Client) Load(lr *command.LoadRequest, nodeAddr string, creds 
*proto.Credentials, timeout time.Duration, retries int) error {
        command := &proto.Command{
                Type: proto.Command_COMMAND_TYPE_LOAD,
@@ -332,7 +339,9 @@
        return nil
 }
 
-// RemoveNode removes a node from the cluster
+// RemoveNode removes a node from the cluster. If creds is nil, then no
+// credential information will be included in the RemoveNode request to the
+// remote node.
 func (c *Client) RemoveNode(rn *command.RemoveNodeRequest, nodeAddr string, 
creds *proto.Credentials, timeout time.Duration) error {
        conn, err := c.dial(nodeAddr)
        if err != nil {
@@ -372,6 +381,8 @@
 }
 
 // Notify notifies a remote node that this node is ready to bootstrap.
+// If creds is nil, then no credential information will be included in
+// // the Notify request to the remote node.
 func (c *Client) Notify(nr *command.NotifyRequest, nodeAddr string, creds 
*proto.Credentials, timeout time.Duration) error {
        conn, err := c.dial(nodeAddr)
        if err != nil {
@@ -411,6 +422,8 @@
 }
 
 // Join joins this node to a cluster at the remote address nodeAddr.
+// If creds is nil, then no credential information will be included in
+// the Join request to the remote node.
 func (c *Client) Join(jr *command.JoinRequest, nodeAddr string, creds 
*proto.Credentials, timeout time.Duration) error {
        for {
                conn, err := c.dial(nodeAddr)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/cluster/proto/message.pb_test.go 
new/rqlite-8.36.14/cluster/proto/message.pb_test.go
--- old/rqlite-8.36.13/cluster/proto/message.pb_test.go 1970-01-01 
01:00:00.000000000 +0100
+++ new/rqlite-8.36.14/cluster/proto/message.pb_test.go 2025-03-15 
16:18:40.000000000 +0100
@@ -0,0 +1,17 @@
+package proto
+
+import (
+       "testing"
+)
+
+// Test_Credentials tests the Credentials methods on a nil Credentials,
+// ensuring it doesn't panic and instead returns empty strings.
+func Test_NilCredentials(t *testing.T) {
+       var creds *Credentials
+       if creds.GetUsername() != "" {
+               t.Fatalf("expected empty username")
+       }
+       if creds.GetPassword() != "" {
+               t.Fatalf("expected empty password")
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/cluster/service.go 
new/rqlite-8.36.14/cluster/service.go
--- old/rqlite-8.36.13/cluster/service.go       2025-03-12 04:47:36.000000000 
+0100
+++ new/rqlite-8.36.14/cluster/service.go       2025-03-15 16:18:40.000000000 
+0100
@@ -250,14 +250,7 @@
        if s.credentialStore == nil {
                return true
        }
-
-       username := ""
-       password := ""
-       if c.Credentials != nil {
-               username = c.Credentials.GetUsername()
-               password = c.Credentials.GetPassword()
-       }
-       return s.credentialStore.AA(username, password, perm)
+       return s.credentialStore.AA(c.Credentials.GetUsername(), 
c.Credentials.GetPassword(), perm)
 }
 
 func (s *Service) checkCommandPermAll(c *proto.Command, perms ...string) bool {
@@ -265,14 +258,8 @@
                return true
        }
 
-       username := ""
-       password := ""
-       if c.Credentials != nil {
-               username = c.Credentials.GetUsername()
-               password = c.Credentials.GetPassword()
-       }
        for _, perm := range perms {
-               if !s.credentialStore.AA(username, password, perm) {
+               if !s.credentialStore.AA(c.Credentials.GetUsername(), 
c.Credentials.GetPassword(), perm) {
                        return false
                }
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/go.mod new/rqlite-8.36.14/go.mod
--- old/rqlite-8.36.13/go.mod   2025-03-12 04:47:36.000000000 +0100
+++ new/rqlite-8.36.14/go.mod   2025-03-15 16:18:40.000000000 +0100
@@ -20,7 +20,7 @@
        github.com/rqlite/rqlite-disco-clients 
v0.0.0-20250205044118-8ada2b350099
        github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196
        go.etcd.io/bbolt v1.4.0
-       golang.org/x/net v0.35.0
+       golang.org/x/net v0.36.0
        google.golang.org/protobuf v1.36.5
 )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/go.sum new/rqlite-8.36.14/go.sum
--- old/rqlite-8.36.13/go.sum   2025-03-12 04:47:36.000000000 +0100
+++ new/rqlite-8.36.14/go.sum   2025-03-15 16:18:40.000000000 +0100
@@ -309,8 +309,8 @@
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod 
h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod 
h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod 
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
+golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 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=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/http/service.go 
new/rqlite-8.36.14/http/service.go
--- old/rqlite-8.36.13/http/service.go  2025-03-12 04:47:36.000000000 +0100
+++ new/rqlite-8.36.14/http/service.go  2025-03-15 16:18:40.000000000 +0100
@@ -595,13 +595,8 @@
                                return
                        }
 
-                       username, password, ok := r.BasicAuth()
-                       if !ok {
-                               username = ""
-                       }
-
                        w.Header().Add(ServedByHTTPHeader, addr)
-                       removeErr := s.cluster.RemoveNode(rn, addr, 
makeCredentials(username, password), qp.Timeout(defaultTimeout))
+                       removeErr := s.cluster.RemoveNode(rn, addr, 
makeCredentials(r), qp.Timeout(defaultTimeout))
                        if removeErr != nil {
                                if removeErr.Error() == "unauthorized" {
                                        http.Error(w, "remote remove node not 
authorized", http.StatusUnauthorized)
@@ -657,13 +652,8 @@
                                return
                        }
 
-                       username, password, ok := r.BasicAuth()
-                       if !ok {
-                               username = ""
-                       }
-
                        w.Header().Add(ServedByHTTPHeader, addr)
-                       backupErr := s.cluster.Backup(br, addr, 
makeCredentials(username, password), qp.Timeout(defaultTimeout), w)
+                       backupErr := s.cluster.Backup(br, addr, 
makeCredentials(r), qp.Timeout(defaultTimeout), w)
                        if backupErr != nil {
                                if backupErr.Error() == "unauthorized" {
                                        http.Error(w, "remote backup not 
authorized", http.StatusUnauthorized)
@@ -697,6 +687,27 @@
                return
        }
 
+       // Determine some perhaps-needed details.
+       ldrAddr, err := s.store.LeaderAddr()
+       if err != nil {
+               http.Error(w, fmt.Sprintf("leader address: %s", err.Error()),
+                       http.StatusInternalServerError)
+               return
+       }
+       if ldrAddr == "" {
+               stats.Add(numLeaderNotFound, 1)
+               http.Error(w, ErrLeaderNotFound.Error(), 
http.StatusServiceUnavailable)
+               return
+       }
+
+       handleRemoteErr := func(err error) {
+               if err.Error() == "unauthorized" {
+                       http.Error(w, "remote load not authorized", 
http.StatusUnauthorized)
+               } else {
+                       http.Error(w, err.Error(), 
http.StatusInternalServerError)
+               }
+       }
+
        resp := NewResponse()
        b, err := io.ReadAll(r.Body)
        if err != nil {
@@ -720,32 +731,10 @@
                                return
                        }
 
-                       addr, err := s.store.LeaderAddr()
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("leader address: %s", 
err.Error()),
-                                       http.StatusInternalServerError)
-                               return
-                       }
-                       if addr == "" {
-                               stats.Add(numLeaderNotFound, 1)
-                               http.Error(w, ErrLeaderNotFound.Error(), 
http.StatusServiceUnavailable)
-                               return
-                       }
-
-                       username, password, ok := r.BasicAuth()
-                       if !ok {
-                               username = ""
-                       }
-
-                       w.Header().Add(ServedByHTTPHeader, addr)
-                       loadErr := s.cluster.Load(lr, addr, 
makeCredentials(username, password),
-                               qp.Timeout(defaultTimeout), qp.Retries(0))
+                       w.Header().Add(ServedByHTTPHeader, ldrAddr)
+                       loadErr := s.cluster.Load(lr, ldrAddr, 
makeCredentials(r), qp.Timeout(defaultTimeout), qp.Retries(0))
                        if loadErr != nil {
-                               if loadErr.Error() == "unauthorized" {
-                                       http.Error(w, "remote load not 
authorized", http.StatusUnauthorized)
-                               } else {
-                                       http.Error(w, loadErr.Error(), 
http.StatusInternalServerError)
-                               }
+                               handleRemoteErr(loadErr)
                                return
                        }
                        stats.Add(numRemoteLoads, 1)
@@ -753,7 +742,7 @@
                        // forwarding was put in place.
                }
        } else {
-               // No JSON structure expected for this API.
+               // No JSON structure expected for this API, just a bunch of SQL 
statements.
                queries := []string{string(b)}
                er := executeRequestFromStrings(queries, qp.Timings(), false)
 
@@ -763,9 +752,24 @@
                                if s.DoRedirect(w, r, qp) {
                                        return
                                }
+
+                               w.Header().Add(ServedByHTTPHeader, ldrAddr)
+                               var exErr error
+                               response, exErr = s.cluster.Execute(er, 
ldrAddr, makeCredentials(r),
+                                       qp.Timeout(defaultTimeout), 
qp.Retries(0))
+                               if exErr != nil {
+                                       handleRemoteErr(exErr)
+                                       return
+                               }
+                               resp.Results.ExecuteQueryResponse = response
+                               stats.Add(numRemoteLoads, 1)
+                       } else {
+                               // Local execute failed for some reason other 
than not
+                               // being the leader. Nothing we can do here.
+                               resp.Error = err.Error()
                        }
-                       resp.Error = err.Error()
                } else {
+                       // Successful local execute.
                        resp.Results.ExecuteQueryResponse = response
                }
                resp.end = time.Now()
@@ -1206,13 +1210,8 @@
                        return
                }
 
-               username, password, ok := r.BasicAuth()
-               if !ok {
-                       username = ""
-               }
-
                w.Header().Add(ServedByHTTPHeader, addr)
-               results, resultsErr = s.cluster.Execute(er, addr, 
makeCredentials(username, password),
+               results, resultsErr = s.cluster.Execute(er, addr, 
makeCredentials(r),
                        qp.Timeout(defaultTimeout), qp.Retries(0))
                if resultsErr != nil {
                        stats.Add(numRemoteExecutionsFailed, 1)
@@ -1316,13 +1315,9 @@
                        http.Error(w, ErrLeaderNotFound.Error(), 
http.StatusServiceUnavailable)
                        return
                }
-               username, password, ok := r.BasicAuth()
-               if !ok {
-                       username = ""
-               }
 
                w.Header().Add(ServedByHTTPHeader, addr)
-               results, resultsErr = s.cluster.Query(qr, addr, 
makeCredentials(username, password), qp.Timeout(defaultTimeout))
+               results, resultsErr = s.cluster.Query(qr, addr, 
makeCredentials(r), qp.Timeout(defaultTimeout))
                if resultsErr != nil {
                        stats.Add(numRemoteQueriesFailed, 1)
                        if resultsErr.Error() == "unauthorized" {
@@ -1403,13 +1398,9 @@
                        http.Error(w, ErrLeaderNotFound.Error(), 
http.StatusServiceUnavailable)
                        return
                }
-               username, password, ok := r.BasicAuth()
-               if !ok {
-                       username = ""
-               }
 
                w.Header().Add(ServedByHTTPHeader, addr)
-               results, resultsErr = s.cluster.Request(eqr, addr, 
makeCredentials(username, password),
+               results, resultsErr = s.cluster.Request(eqr, addr, 
makeCredentials(r),
                        qp.Timeout(defaultTimeout), qp.Retries(0))
                if resultsErr != nil {
                        stats.Add(numRemoteRequestsFailed, 1)
@@ -1793,7 +1784,11 @@
        }
 }
 
-func makeCredentials(username, password string) *clstrPB.Credentials {
+func makeCredentials(r *http.Request) *clstrPB.Credentials {
+       username, password, ok := r.BasicAuth()
+       if !ok {
+               return nil
+       }
        return &clstrPB.Credentials{
                Username: username,
                Password: password,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/http/service_test.go 
new/rqlite-8.36.14/http/service_test.go
--- old/rqlite-8.36.13/http/service_test.go     2025-03-12 04:47:36.000000000 
+0100
+++ new/rqlite-8.36.14/http/service_test.go     2025-03-15 16:18:40.000000000 
+0100
@@ -709,7 +709,9 @@
 }
 
 func Test_LoadOK(t *testing.T) {
-       m := &MockStore{}
+       m := &MockStore{
+               leaderAddr: "foo:1234",
+       }
        c := &mockClusterService{}
        s := New("127.0.0.1:0", m, c, nil)
        if err := s.Start(); err != nil {
@@ -725,7 +727,7 @@
        }
        defer resp.Body.Close()
        if resp.StatusCode != http.StatusOK {
-               t.Fatalf("failed to get expected StatusOK for load, got %d", 
resp.StatusCode)
+               t.Fatalf("failed to get expected StatusOK for load, got %d, 
%s", resp.StatusCode, mustReadBody(t, resp))
        }
        if exp, got := `{"results":[]}`, mustReadBody(t, resp); exp != got {
                t.Fatalf("incorrect response body, exp: %s, got %s", exp, got)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/system_test/cluster_test.go 
new/rqlite-8.36.14/system_test/cluster_test.go
--- old/rqlite-8.36.13/system_test/cluster_test.go      2025-03-12 
04:47:36.000000000 +0100
+++ new/rqlite-8.36.14/system_test/cluster_test.go      2025-03-15 
16:18:40.000000000 +0100
@@ -5,6 +5,7 @@
        "fmt"
        "net"
        "os"
+       "path/filepath"
        "sync"
        "testing"
        "time"
@@ -1456,6 +1457,39 @@
        }
 }
 
+func Test_MultiNodeCluster_FollowerLoad_SQL(t *testing.T) {
+       node1 := mustNewLeaderNode("leader1")
+       defer node1.Deprovision()
+
+       node2 := mustNewNode("node2", false)
+       defer node2.Deprovision()
+       if err := node2.Join(node1); err != nil {
+               t.Fatalf("node failed to join leader: %s", err.Error())
+       }
+       _, err := node2.WaitForLeader()
+       if err != nil {
+               t.Fatalf("failed waiting for leader: %s", err.Error())
+       }
+
+       // Get a follower, make sure Load works via it.
+       c := Cluster{node1, node2}
+       followers, err := c.Followers()
+       if err != nil {
+               t.Fatalf("failed to get followers: %s", err.Error())
+       }
+       if _, err := followers[0].Load(filepath.Join("testdata", 
"auto-restore.sql")); err != nil {
+               t.Fatalf("failed to load via follower: %s", err.Error())
+       }
+
+       r, err := node1.QueryStrongConsistency("SELECT * FROM foo WHERE id=2")
+       if err != nil {
+               t.Fatalf("failed to execute query: %s", err.Error())
+       }
+       if r != 
`{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[2,"fiona"]]}]}`
 {
+               t.Fatalf("test received wrong result got %s", r)
+       }
+}
+
 // Test_MultiNodeClusterWithNonVoter tests formation of a 4-node cluster, one 
of which is
 // a non-voter. This test also checks that if the Leader changes the non-voter 
is still in
 // the cluster and gets updates from the new leader.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/system_test/helpers.go 
new/rqlite-8.36.14/system_test/helpers.go
--- old/rqlite-8.36.13/system_test/helpers.go   2025-03-12 04:47:36.000000000 
+0100
+++ new/rqlite-8.36.14/system_test/helpers.go   2025-03-15 16:18:40.000000000 
+0100
@@ -267,7 +267,7 @@
        return err
 }
 
-// Load loads a SQLite database file into the node.
+// Boot boots a node using a SQLite database file.
 func (n *Node) Boot(filename string) (string, error) {
        return n.postFile("/boot", filename)
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/system_test/single_node_test.go 
new/rqlite-8.36.14/system_test/single_node_test.go
--- old/rqlite-8.36.13/system_test/single_node_test.go  2025-03-12 
04:47:36.000000000 +0100
+++ new/rqlite-8.36.14/system_test/single_node_test.go  2025-03-15 
16:18:40.000000000 +0100
@@ -1823,3 +1823,21 @@
                t.Fatalf("expected error loading data")
        }
 }
+
+func Test_SingleNodeLoad_OK(t *testing.T) {
+       node := mustNewLeaderNode("leader1")
+       defer node.Deprovision()
+
+       _, err := node.Load(filepath.Join("testdata", "auto-restore.sql"))
+       if err != nil {
+               t.Fatalf("failed to load data: %s", err.Error())
+       }
+
+       r, err := node.Query("SELECT * FROM foo WHERE id=2")
+       if err != nil {
+               t.Fatalf("failed to execute query: %s", err.Error())
+       }
+       if r != 
`{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[2,"fiona"]]}]}`
 {
+               t.Fatalf("test received wrong result got %s", r)
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rqlite-8.36.13/system_test/testdata/auto-restore.sql 
new/rqlite-8.36.14/system_test/testdata/auto-restore.sql
--- old/rqlite-8.36.13/system_test/testdata/auto-restore.sql    1970-01-01 
01:00:00.000000000 +0100
+++ new/rqlite-8.36.14/system_test/testdata/auto-restore.sql    2025-03-15 
16:18:40.000000000 +0100
@@ -0,0 +1,7 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE foo (id integer not null primary key, name text);
+INSERT INTO foo VALUES(1,'fiona');
+INSERT INTO foo VALUES(2,'fiona');
+INSERT INTO foo VALUES(3,'fiona');
+COMMIT;

++++++ vendor.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/golang.org/x/net/http2/server.go 
new/vendor/golang.org/x/net/http2/server.go
--- old/vendor/golang.org/x/net/http2/server.go 2025-03-12 21:36:00.000000000 
+0100
+++ new/vendor/golang.org/x/net/http2/server.go 2025-03-16 14:43:35.000000000 
+0100
@@ -2233,25 +2233,25 @@
 func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) 
(*responseWriter, *http.Request, error) {
        sc.serveG.check()
 
-       rp := requestParam{
-               method:    f.PseudoValue("method"),
-               scheme:    f.PseudoValue("scheme"),
-               authority: f.PseudoValue("authority"),
-               path:      f.PseudoValue("path"),
-               protocol:  f.PseudoValue("protocol"),
+       rp := httpcommon.ServerRequestParam{
+               Method:    f.PseudoValue("method"),
+               Scheme:    f.PseudoValue("scheme"),
+               Authority: f.PseudoValue("authority"),
+               Path:      f.PseudoValue("path"),
+               Protocol:  f.PseudoValue("protocol"),
        }
 
        // extended connect is disabled, so we should not see :protocol
-       if disableExtendedConnectProtocol && rp.protocol != "" {
+       if disableExtendedConnectProtocol && rp.Protocol != "" {
                return nil, nil, sc.countError("bad_connect", 
streamError(f.StreamID, ErrCodeProtocol))
        }
 
-       isConnect := rp.method == "CONNECT"
+       isConnect := rp.Method == "CONNECT"
        if isConnect {
-               if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || 
rp.authority == "") {
+               if rp.Protocol == "" && (rp.Path != "" || rp.Scheme != "" || 
rp.Authority == "") {
                        return nil, nil, sc.countError("bad_connect", 
streamError(f.StreamID, ErrCodeProtocol))
                }
-       } else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && 
rp.scheme != "http") {
+       } else if rp.Method == "" || rp.Path == "" || (rp.Scheme != "https" && 
rp.Scheme != "http") {
                // See 8.1.2.6 Malformed Requests and Responses:
                //
                // Malformed requests or responses that are detected
@@ -2265,15 +2265,16 @@
                return nil, nil, sc.countError("bad_path_method", 
streamError(f.StreamID, ErrCodeProtocol))
        }
 
-       rp.header = make(http.Header)
+       header := make(http.Header)
+       rp.Header = header
        for _, hf := range f.RegularFields() {
-               rp.header.Add(sc.canonicalHeader(hf.Name), hf.Value)
+               header.Add(sc.canonicalHeader(hf.Name), hf.Value)
        }
-       if rp.authority == "" {
-               rp.authority = rp.header.Get("Host")
+       if rp.Authority == "" {
+               rp.Authority = header.Get("Host")
        }
-       if rp.protocol != "" {
-               rp.header.Set(":protocol", rp.protocol)
+       if rp.Protocol != "" {
+               header.Set(":protocol", rp.Protocol)
        }
 
        rw, req, err := sc.newWriterAndRequestNoBody(st, rp)
@@ -2282,7 +2283,7 @@
        }
        bodyOpen := !f.StreamEnded()
        if bodyOpen {
-               if vv, ok := rp.header["Content-Length"]; ok {
+               if vv, ok := rp.Header["Content-Length"]; ok {
                        if cl, err := strconv.ParseUint(vv[0], 10, 63); err == 
nil {
                                req.ContentLength = int64(cl)
                        } else {
@@ -2298,84 +2299,38 @@
        return rw, req, nil
 }
 
-type requestParam struct {
-       method                  string
-       scheme, authority, path string
-       protocol                string
-       header                  http.Header
-}
-
-func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) 
(*responseWriter, *http.Request, error) {
+func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp 
httpcommon.ServerRequestParam) (*responseWriter, *http.Request, error) {
        sc.serveG.check()
 
        var tlsState *tls.ConnectionState // nil if not scheme https
-       if rp.scheme == "https" {
+       if rp.Scheme == "https" {
                tlsState = sc.tlsState
        }
 
-       needsContinue := 
httpguts.HeaderValuesContainsToken(rp.header["Expect"], "100-continue")
-       if needsContinue {
-               rp.header.Del("Expect")
-       }
-       // Merge Cookie headers into one "; "-delimited value.
-       if cookies := rp.header["Cookie"]; len(cookies) > 1 {
-               rp.header.Set("Cookie", strings.Join(cookies, "; "))
-       }
-
-       // Setup Trailers
-       var trailer http.Header
-       for _, v := range rp.header["Trailer"] {
-               for _, key := range strings.Split(v, ",") {
-                       key = http.CanonicalHeaderKey(textproto.TrimString(key))
-                       switch key {
-                       case "Transfer-Encoding", "Trailer", "Content-Length":
-                               // Bogus. (copy of http1 rules)
-                               // Ignore.
-                       default:
-                               if trailer == nil {
-                                       trailer = make(http.Header)
-                               }
-                               trailer[key] = nil
-                       }
-               }
-       }
-       delete(rp.header, "Trailer")
-
-       var url_ *url.URL
-       var requestURI string
-       if rp.method == "CONNECT" && rp.protocol == "" {
-               url_ = &url.URL{Host: rp.authority}
-               requestURI = rp.authority // mimic HTTP/1 server behavior
-       } else {
-               var err error
-               url_, err = url.ParseRequestURI(rp.path)
-               if err != nil {
-                       return nil, nil, sc.countError("bad_path", 
streamError(st.id, ErrCodeProtocol))
-               }
-               requestURI = rp.path
+       res := httpcommon.NewServerRequest(rp)
+       if res.InvalidReason != "" {
+               return nil, nil, sc.countError(res.InvalidReason, 
streamError(st.id, ErrCodeProtocol))
        }
 
        body := &requestBody{
                conn:          sc,
                stream:        st,
-               needsContinue: needsContinue,
+               needsContinue: res.NeedsContinue,
        }
-       req := &http.Request{
-               Method:     rp.method,
-               URL:        url_,
+       req := (&http.Request{
+               Method:     rp.Method,
+               URL:        res.URL,
                RemoteAddr: sc.remoteAddrStr,
-               Header:     rp.header,
-               RequestURI: requestURI,
+               Header:     rp.Header,
+               RequestURI: res.RequestURI,
                Proto:      "HTTP/2.0",
                ProtoMajor: 2,
                ProtoMinor: 0,
                TLS:        tlsState,
-               Host:       rp.authority,
+               Host:       rp.Authority,
                Body:       body,
-               Trailer:    trailer,
-       }
-       req = req.WithContext(st.ctx)
-
+               Trailer:    res.Trailer,
+       }).WithContext(st.ctx)
        rw := sc.newResponseWriter(st, req)
        return rw, req, nil
 }
@@ -3270,12 +3225,12 @@
                // we start in "half closed (remote)" for simplicity.
                // See further comments at the definition of 
stateHalfClosedRemote.
                promised := sc.newStream(promisedID, msg.parent.id, 
stateHalfClosedRemote)
-               rw, req, err := sc.newWriterAndRequestNoBody(promised, 
requestParam{
-                       method:    msg.method,
-                       scheme:    msg.url.Scheme,
-                       authority: msg.url.Host,
-                       path:      msg.url.RequestURI(),
-                       header:    cloneHeader(msg.header), // clone since 
handler runs concurrently with writing the PUSH_PROMISE
+               rw, req, err := sc.newWriterAndRequestNoBody(promised, 
httpcommon.ServerRequestParam{
+                       Method:    msg.method,
+                       Scheme:    msg.url.Scheme,
+                       Authority: msg.url.Host,
+                       Path:      msg.url.RequestURI(),
+                       Header:    cloneHeader(msg.header), // clone since 
handler runs concurrently with writing the PUSH_PROMISE
                })
                if err != nil {
                        // Should not happen, since we've already validated 
msg.url.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/golang.org/x/net/http2/transport.go 
new/vendor/golang.org/x/net/http2/transport.go
--- old/vendor/golang.org/x/net/http2/transport.go      2025-03-12 
21:36:00.000000000 +0100
+++ new/vendor/golang.org/x/net/http2/transport.go      2025-03-16 
14:43:35.000000000 +0100
@@ -1286,6 +1286,19 @@
        return 0
 }
 
+// actualContentLength returns a sanitized version of
+// req.ContentLength, where 0 actually means zero (not unknown) and -1
+// means unknown.
+func actualContentLength(req *http.Request) int64 {
+       if req.Body == nil || req.Body == http.NoBody {
+               return 0
+       }
+       if req.ContentLength != 0 {
+               return req.ContentLength
+       }
+       return -1
+}
+
 func (cc *ClientConn) decrStreamReservations() {
        cc.mu.Lock()
        defer cc.mu.Unlock()
@@ -1310,7 +1323,7 @@
                reqCancel:            req.Cancel,
                isHead:               req.Method == "HEAD",
                reqBody:              req.Body,
-               reqBodyContentLength: httpcommon.ActualContentLength(req),
+               reqBodyContentLength: actualContentLength(req),
                trace:                httptrace.ContextClientTrace(ctx),
                peerClosed:           make(chan struct{}),
                abort:                make(chan struct{}),
@@ -1318,7 +1331,7 @@
                donec:                make(chan struct{}),
        }
 
-       cs.requestedGzip = httpcommon.IsRequestGzip(req, 
cc.t.disableCompression())
+       cs.requestedGzip = httpcommon.IsRequestGzip(req.Method, req.Header, 
cc.t.disableCompression())
 
        go cs.doRequest(req, streamf)
 
@@ -1349,7 +1362,7 @@
                }
                res.Request = req
                res.TLS = cc.tlsState
-               if res.Body == noBody && httpcommon.ActualContentLength(req) == 
0 {
+               if res.Body == noBody && actualContentLength(req) == 0 {
                        // If there isn't a request or response body still being
                        // written, then wait for the stream to be closed before
                        // RoundTrip returns.
@@ -1596,12 +1609,7 @@
        // sent by writeRequestBody below, along with any Trailers,
        // again in form HEADERS{1}, CONTINUATION{0,})
        cc.hbuf.Reset()
-       res, err := httpcommon.EncodeHeaders(httpcommon.EncodeHeadersParam{
-               Request:               req,
-               AddGzipHeader:         cs.requestedGzip,
-               PeerMaxHeaderListSize: cc.peerMaxHeaderListSize,
-               DefaultUserAgent:      defaultUserAgent,
-       }, func(name, value string) {
+       res, err := encodeRequestHeaders(req, cs.requestedGzip, 
cc.peerMaxHeaderListSize, func(name, value string) {
                cc.writeHeader(name, value)
        })
        if err != nil {
@@ -1617,6 +1625,22 @@
        return err
 }
 
+func encodeRequestHeaders(req *http.Request, addGzipHeader bool, 
peerMaxHeaderListSize uint64, headerf func(name, value string)) 
(httpcommon.EncodeHeadersResult, error) {
+       return httpcommon.EncodeHeaders(req.Context(), 
httpcommon.EncodeHeadersParam{
+               Request: httpcommon.Request{
+                       Header:              req.Header,
+                       Trailer:             req.Trailer,
+                       URL:                 req.URL,
+                       Host:                req.Host,
+                       Method:              req.Method,
+                       ActualContentLength: actualContentLength(req),
+               },
+               AddGzipHeader:         addGzipHeader,
+               PeerMaxHeaderListSize: peerMaxHeaderListSize,
+               DefaultUserAgent:      defaultUserAgent,
+       }, headerf)
+}
+
 // cleanupWriteRequest performs post-request tasks.
 //
 // If err (the result of writeRequest) is non-nil and the stream is not closed,
@@ -2186,6 +2210,13 @@
        }
        cc.cond.Broadcast()
        cc.mu.Unlock()
+
+       if !cc.seenSettings {
+               // If we have a pending request that wants extended CONNECT,
+               // let it continue and fail with the connection error.
+               cc.extendedConnectAllowed = true
+               close(cc.seenSettingsChan)
+       }
 }
 
 // countReadFrameError calls Transport.CountError with a string
@@ -2278,9 +2309,6 @@
                        if VerboseLogs {
                                cc.vlogf("http2: Transport conn %p received 
error from processing frame %v: %v", cc, summarizeFrame(f), err)
                        }
-                       if !cc.seenSettings {
-                               close(cc.seenSettingsChan)
-                       }
                        return err
                }
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vendor/golang.org/x/net/internal/httpcommon/headermap.go 
new/vendor/golang.org/x/net/internal/httpcommon/headermap.go
--- old/vendor/golang.org/x/net/internal/httpcommon/headermap.go        
2025-03-12 21:36:00.000000000 +0100
+++ new/vendor/golang.org/x/net/internal/httpcommon/headermap.go        
2025-03-16 14:43:35.000000000 +0100
@@ -5,7 +5,7 @@
 package httpcommon
 
 import (
-       "net/http"
+       "net/textproto"
        "sync"
 )
 
@@ -82,7 +82,7 @@
        commonLowerHeader = make(map[string]string, len(common))
        commonCanonHeader = make(map[string]string, len(common))
        for _, v := range common {
-               chk := http.CanonicalHeaderKey(v)
+               chk := textproto.CanonicalMIMEHeaderKey(v)
                commonLowerHeader[chk] = v
                commonCanonHeader[v] = chk
        }
@@ -104,7 +104,7 @@
        if s, ok := commonCanonHeader[v]; ok {
                return s
        }
-       return http.CanonicalHeaderKey(v)
+       return textproto.CanonicalMIMEHeaderKey(v)
 }
 
 // CachedCanonicalHeader returns the canonical form of a well-known header 
name.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vendor/golang.org/x/net/internal/httpcommon/request.go 
new/vendor/golang.org/x/net/internal/httpcommon/request.go
--- old/vendor/golang.org/x/net/internal/httpcommon/request.go  2025-03-12 
21:36:00.000000000 +0100
+++ new/vendor/golang.org/x/net/internal/httpcommon/request.go  2025-03-16 
14:43:35.000000000 +0100
@@ -5,10 +5,12 @@
 package httpcommon
 
 import (
+       "context"
        "errors"
        "fmt"
-       "net/http"
        "net/http/httptrace"
+       "net/textproto"
+       "net/url"
        "sort"
        "strconv"
        "strings"
@@ -21,9 +23,21 @@
        ErrRequestHeaderListSize = errors.New("request header list larger than 
peer's advertised limit")
 )
 
+// Request is a subset of http.Request.
+// It'd be simpler to pass an *http.Request, of course, but we can't depend on 
net/http
+// without creating a dependency cycle.
+type Request struct {
+       URL                 *url.URL
+       Method              string
+       Host                string
+       Header              map[string][]string
+       Trailer             map[string][]string
+       ActualContentLength int64 // 0 means 0, -1 means unknown
+}
+
 // EncodeHeadersParam is parameters to EncodeHeaders.
 type EncodeHeadersParam struct {
-       Request *http.Request
+       Request Request
 
        // AddGzipHeader indicates that an "accept-encoding: gzip" header 
should be
        // added to the request.
@@ -47,11 +61,11 @@
 // It validates a request and calls headerf with each pseudo-header and header
 // for the request.
 // The headerf function is called with the validated, canonicalized header 
name.
-func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) 
(res EncodeHeadersResult, _ error) {
+func EncodeHeaders(ctx context.Context, param EncodeHeadersParam, headerf 
func(name, value string)) (res EncodeHeadersResult, _ error) {
        req := param.Request
 
        // Check for invalid connection-level headers.
-       if err := checkConnHeaders(req); err != nil {
+       if err := checkConnHeaders(req.Header); err != nil {
                return res, err
        }
 
@@ -73,7 +87,10 @@
 
        // isNormalConnect is true if this is a non-extended CONNECT request.
        isNormalConnect := false
-       protocol := req.Header.Get(":protocol")
+       var protocol string
+       if vv := req.Header[":protocol"]; len(vv) > 0 {
+               protocol = vv[0]
+       }
        if req.Method == "CONNECT" && protocol == "" {
                isNormalConnect = true
        } else if protocol != "" && req.Method != "CONNECT" {
@@ -107,9 +124,7 @@
                return res, fmt.Errorf("invalid HTTP trailer %s", err)
        }
 
-       contentLength := ActualContentLength(req)
-
-       trailers, err := commaSeparatedTrailers(req)
+       trailers, err := commaSeparatedTrailers(req.Trailer)
        if err != nil {
                return res, err
        }
@@ -123,7 +138,7 @@
                f(":authority", host)
                m := req.Method
                if m == "" {
-                       m = http.MethodGet
+                       m = "GET"
                }
                f(":method", m)
                if !isNormalConnect {
@@ -198,8 +213,8 @@
                                f(k, v)
                        }
                }
-               if shouldSendReqContentLength(req.Method, contentLength) {
-                       f("content-length", strconv.FormatInt(contentLength, 
10))
+               if shouldSendReqContentLength(req.Method, 
req.ActualContentLength) {
+                       f("content-length", 
strconv.FormatInt(req.ActualContentLength, 10))
                }
                if param.AddGzipHeader {
                        f("accept-encoding", "gzip")
@@ -225,7 +240,7 @@
                }
        }
 
-       trace := httptrace.ContextClientTrace(req.Context())
+       trace := httptrace.ContextClientTrace(ctx)
 
        // Header list size is ok. Write the headers.
        enumerateHeaders(func(name, value string) {
@@ -243,19 +258,19 @@
                }
        })
 
-       res.HasBody = contentLength != 0
+       res.HasBody = req.ActualContentLength != 0
        res.HasTrailers = trailers != ""
        return res, nil
 }
 
 // IsRequestGzip reports whether we should add an Accept-Encoding: gzip header
 // for a request.
-func IsRequestGzip(req *http.Request, disableCompression bool) bool {
+func IsRequestGzip(method string, header map[string][]string, 
disableCompression bool) bool {
        // TODO(bradfitz): this is a copy of the logic in net/http. Unify 
somewhere?
        if !disableCompression &&
-               req.Header.Get("Accept-Encoding") == "" &&
-               req.Header.Get("Range") == "" &&
-               req.Method != "HEAD" {
+               len(header["Accept-Encoding"]) == 0 &&
+               len(header["Range"]) == 0 &&
+               method != "HEAD" {
                // Request gzip only, not deflate. Deflate is ambiguous and
                // not as universally supported anyway.
                // See: https://zlib.net/zlib_faq.html#faq39
@@ -280,22 +295,22 @@
 //
 // Certain headers are special-cased as okay but not transmitted later.
 // For example, we allow "Transfer-Encoding: chunked", but drop the header 
when encoding.
-func checkConnHeaders(req *http.Request) error {
-       if v := req.Header.Get("Upgrade"); v != "" {
-               return fmt.Errorf("invalid Upgrade request header: %q", 
req.Header["Upgrade"])
+func checkConnHeaders(h map[string][]string) error {
+       if vv := h["Upgrade"]; len(vv) > 0 && (vv[0] != "" && vv[0] != 
"chunked") {
+               return fmt.Errorf("invalid Upgrade request header: %q", vv)
        }
-       if vv := req.Header["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 
|| vv[0] != "" && vv[0] != "chunked") {
+       if vv := h["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] 
!= "" && vv[0] != "chunked") {
                return fmt.Errorf("invalid Transfer-Encoding request header: 
%q", vv)
        }
-       if vv := req.Header["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] 
!= "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], 
"keep-alive")) {
+       if vv := h["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && 
!asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) {
                return fmt.Errorf("invalid Connection request header: %q", vv)
        }
        return nil
 }
 
-func commaSeparatedTrailers(req *http.Request) (string, error) {
-       keys := make([]string, 0, len(req.Trailer))
-       for k := range req.Trailer {
+func commaSeparatedTrailers(trailer map[string][]string) (string, error) {
+       keys := make([]string, 0, len(trailer))
+       for k := range trailer {
                k = CanonicalHeader(k)
                switch k {
                case "Transfer-Encoding", "Trailer", "Content-Length":
@@ -310,19 +325,6 @@
        return "", nil
 }
 
-// ActualContentLength returns a sanitized version of
-// req.ContentLength, where 0 actually means zero (not unknown) and -1
-// means unknown.
-func ActualContentLength(req *http.Request) int64 {
-       if req.Body == nil || req.Body == http.NoBody {
-               return 0
-       }
-       if req.ContentLength != 0 {
-               return req.ContentLength
-       }
-       return -1
-}
-
 // validPseudoPath reports whether v is a valid :path pseudo-header
 // value. It must be either:
 //
@@ -340,7 +342,7 @@
        return (len(v) > 0 && v[0] == '/') || v == "*"
 }
 
-func validateHeaders(hdrs http.Header) string {
+func validateHeaders(hdrs map[string][]string) string {
        for k, vv := range hdrs {
                if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
                        return fmt.Sprintf("name %q", k)
@@ -377,3 +379,89 @@
                return false
        }
 }
+
+// ServerRequestParam is parameters to NewServerRequest.
+type ServerRequestParam struct {
+       Method                  string
+       Scheme, Authority, Path string
+       Protocol                string
+       Header                  map[string][]string
+}
+
+// ServerRequestResult is the result of NewServerRequest.
+type ServerRequestResult struct {
+       // Various http.Request fields.
+       URL        *url.URL
+       RequestURI string
+       Trailer    map[string][]string
+
+       NeedsContinue bool // client provided an "Expect: 100-continue" header
+
+       // If the request should be rejected, this is a short string suitable 
for passing
+       // to the http2 package's CountError function.
+       // It might be a bit odd to return errors this way rather than returing 
an error,
+       // but this ensures we don't forget to include a CountError reason.
+       InvalidReason string
+}
+
+func NewServerRequest(rp ServerRequestParam) ServerRequestResult {
+       needsContinue := 
httpguts.HeaderValuesContainsToken(rp.Header["Expect"], "100-continue")
+       if needsContinue {
+               delete(rp.Header, "Expect")
+       }
+       // Merge Cookie headers into one "; "-delimited value.
+       if cookies := rp.Header["Cookie"]; len(cookies) > 1 {
+               rp.Header["Cookie"] = []string{strings.Join(cookies, "; ")}
+       }
+
+       // Setup Trailers
+       var trailer map[string][]string
+       for _, v := range rp.Header["Trailer"] {
+               for _, key := range strings.Split(v, ",") {
+                       key = 
textproto.CanonicalMIMEHeaderKey(textproto.TrimString(key))
+                       switch key {
+                       case "Transfer-Encoding", "Trailer", "Content-Length":
+                               // Bogus. (copy of http1 rules)
+                               // Ignore.
+                       default:
+                               if trailer == nil {
+                                       trailer = make(map[string][]string)
+                               }
+                               trailer[key] = nil
+                       }
+               }
+       }
+       delete(rp.Header, "Trailer")
+
+       // "':authority' MUST NOT include the deprecated userinfo subcomponent
+       // for "http" or "https" schemed URIs."
+       // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8
+       if strings.IndexByte(rp.Authority, '@') != -1 && (rp.Scheme == "http" 
|| rp.Scheme == "https") {
+               return ServerRequestResult{
+                       InvalidReason: "userinfo_in_authority",
+               }
+       }
+
+       var url_ *url.URL
+       var requestURI string
+       if rp.Method == "CONNECT" && rp.Protocol == "" {
+               url_ = &url.URL{Host: rp.Authority}
+               requestURI = rp.Authority // mimic HTTP/1 server behavior
+       } else {
+               var err error
+               url_, err = url.ParseRequestURI(rp.Path)
+               if err != nil {
+                       return ServerRequestResult{
+                               InvalidReason: "bad_path",
+                       }
+               }
+               requestURI = rp.Path
+       }
+
+       return ServerRequestResult{
+               URL:           url_,
+               NeedsContinue: needsContinue,
+               RequestURI:    requestURI,
+               Trailer:       trailer,
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt
--- old/vendor/modules.txt      2025-03-12 21:36:01.000000000 +0100
+++ new/vendor/modules.txt      2025-03-16 14:43:39.000000000 +0100
@@ -279,8 +279,8 @@
 # golang.org/x/exp v0.0.0-20250228200357-dead58393ab7
 ## explicit; go 1.23.0
 golang.org/x/exp/slices
-# golang.org/x/net v0.35.0
-## explicit; go 1.18
+# golang.org/x/net v0.36.0
+## explicit; go 1.23.0
 golang.org/x/net/http/httpguts
 golang.org/x/net/http2
 golang.org/x/net/http2/hpack

Reply via email to