This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch GLVBehaviouralAlignment
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit d11ebfd6c3ec88ed5e70644bf847925d5b078925
Author: Cole Greer <[email protected]>
AuthorDate: Tue Jun 2 12:37:34 2026 -0700

    Add request timeout and empty-response handling to gremlin-go
    
    - Add a RequestTimeout option wired to http.Transport.ResponseHeaderTimeout 
so a
      server that accepts the connection but never responds surfaces a timeout
      instead of hanging (tinkerpop-y32).
    - Treat an empty HTTP response body as an error instead of returning an 
empty
      result set (tinkerpop-zar).
    - Unskip and tighten the corresponding behavioral tests.
---
 gremlin-go/driver/client.go               |  7 +++++++
 gremlin-go/driver/client_behavior_test.go | 32 +++++++++++++++++++------------
 gremlin-go/driver/connection.go           | 11 ++++++++++-
 gremlin-go/driver/connection_test.go      |  2 ++
 4 files changed, 39 insertions(+), 13 deletions(-)

diff --git a/gremlin-go/driver/client.go b/gremlin-go/driver/client.go
index d5345393ad..83e5d21054 100644
--- a/gremlin-go/driver/client.go
+++ b/gremlin-go/driver/client.go
@@ -115,6 +115,12 @@ type ClientSettings struct {
        // uses http.ProxyFromEnvironment (HTTP_PROXY/HTTPS_PROXY/NO_PROXY).
        Proxy func(*http.Request) (*url.URL, error)
 
+       // RequestTimeout is the maximum time to wait for a response after 
sending a request.
+       // This bounds the time between finishing writing the request and 
receiving the response
+       // headers from the server. It is independent of ConnectionTimeout 
which only governs
+       // connection establishment. Set to 0 to disable (no timeout). Default: 
0 (disabled).
+       RequestTimeout time.Duration
+
        EnableUserAgentOnConnect bool
 
        // PDTRegistry enables automatic hydration of CompositePDT values 
during deserialization.
@@ -183,6 +189,7 @@ func NewClient(url string, configurations ...func(settings 
*ClientSettings)) (*C
                ssl:                      settings.Ssl,
                connectTimeout:           connectTimeout,
                readTimeout:              readTimeout,
+               requestTimeout:           settings.RequestTimeout,
                maxConnsPerHost:          settings.MaxConnections,
                maxIdleConnsPerHost:      settings.MaxIdleConnections,
                idleTimeout:              idleTimeout,
diff --git a/gremlin-go/driver/client_behavior_test.go 
b/gremlin-go/driver/client_behavior_test.go
index f274c0ad2c..6e35a43f76 100644
--- a/gremlin-go/driver/client_behavior_test.go
+++ b/gremlin-go/driver/client_behavior_test.go
@@ -159,14 +159,10 @@ func TestShouldHandleEmptyResponseBody(t *testing.T) {
                done <- submitExpectErr(client, gremlinEmptyBody)
        }()
 
-       // The key requirement is that an empty response body does not hang.
-       // NOTE: Unlike the Java/Python/JS drivers (which raise an error), the 
Go
-       // driver currently treats an empty body as an empty (successful) result
-       // set rather than an error. This driver gap is flagged in the cross-GLV
-       // error-message audit (tinkerpop-8lw.6) for further consideration.
        select {
-       case <-done:
-               // completed without hanging - acceptable for now
+       case submitErr := <-done:
+               require.Error(t, submitErr)
+               assert.Contains(t, submitErr.Error(), "empty response body")
        case <-ctx.Done():
                t.Fatal("request hung on empty response body")
        }
@@ -186,11 +182,23 @@ func TestShouldHandleSlowResponse(t *testing.T) {
 }
 
 func TestShouldTimeoutWhenServerNeverResponds(t *testing.T) {
-       // The Go driver's ConnectionTimeout only governs connection 
establishment,
-       // not how long to wait for a response. With no client-side request/read
-       // timeout, a server that never responds causes an indefinite hang. 
Skipped
-       // until the driver supports a request timeout (flagged in 
tinkerpop-8lw.6).
-       t.Skip("Go driver lacks a client-side request/read timeout")
+       url := socketServerURL()
+       client, err := NewClient(url, func(settings *ClientSettings) {
+               settings.RequestTimeout = 2 * time.Second
+       })
+       if err != nil {
+               t.Skip("Socket server not available")
+       }
+       defer client.Close()
+
+       // Verify connectivity before testing the no-response scenario
+       if err := submitExpectErr(client, gremlinSingleVertex); err != nil {
+               t.Skip("Socket server not available")
+       }
+
+       err = submitExpectErr(client, gremlinNoResponse)
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "timeout")
 }
 
 func TestShouldHandleAsyncRequestsDuringConnectionClose(t *testing.T) {
diff --git a/gremlin-go/driver/connection.go b/gremlin-go/driver/connection.go
index b00852fb65..6da5429c4e 100644
--- a/gremlin-go/driver/connection.go
+++ b/gremlin-go/driver/connection.go
@@ -50,6 +50,7 @@ type connectionSettings struct {
        ssl                      *tls.Config
        connectTimeout           time.Duration
        readTimeout              time.Duration
+       requestTimeout           time.Duration
        maxConnsPerHost          int
        maxIdleConnsPerHost      int
        idleTimeout              time.Duration
@@ -165,6 +166,10 @@ func newConnection(handler *logHandler, url string, 
connSettings *connectionSett
                // generic HTTP compression, so the manual decode path in 
getReader handles
                // decompression. Disable net/http's transparent (gzip-only) 
handling.
                DisableCompression: true,
+               // Bounds the time between finishing writing the request and 
receiving response
+               // headers. Independent of connectTimeout, which only governs 
connection
+               // establishment. Zero disables the timeout.
+               ResponseHeaderTimeout: connSettings.requestTimeout,
        }
 
        return &connection{
@@ -395,7 +400,11 @@ func (c *connection) streamToResultSet(reader io.Reader, 
rs ResultSet) {
                d = NewGraphBinaryDeserializer(reader)
        }
        if err := d.ReadHeader(); err != nil {
-               if err != io.EOF {
+               if err == io.EOF {
+                       emptyBodyErr := fmt.Errorf("received empty response 
body from server")
+                       c.logHandler.logf(Error, failedToReceiveResponse, 
emptyBodyErr.Error())
+                       rs.setError(emptyBodyErr)
+               } else {
                        c.logHandler.logf(Error, failedToReceiveResponse, 
err.Error())
                        rs.setError(err)
                }
diff --git a/gremlin-go/driver/connection_test.go 
b/gremlin-go/driver/connection_test.go
index f3ca4094ca..6182a5bcaf 100644
--- a/gremlin-go/driver/connection_test.go
+++ b/gremlin-go/driver/connection_test.go
@@ -1229,6 +1229,7 @@ func TestConnectionPoolSettings(t *testing.T) {
                        idleTimeout:         300 * time.Second,
                        keepAliveTime:       60 * time.Second,
                        connectTimeout:      30 * time.Second,
+                       requestTimeout:      5 * time.Second,
                }
 
                conn := newConnection(newTestLogHandler(), 
"http://localhost:8182/gremlin";, customSettings)
@@ -1239,6 +1240,7 @@ func TestConnectionPoolSettings(t *testing.T) {
                assert.Equal(t, 256, transport.MaxConnsPerHost, 
"MaxConnsPerHost should be custom value")
                assert.Equal(t, 16, transport.MaxIdleConnsPerHost, 
"MaxIdleConnsPerHost should be custom value")
                assert.Equal(t, 300*time.Second, transport.IdleConnTimeout, 
"IdleConnTimeout should be custom value")
+               assert.Equal(t, 5*time.Second, transport.ResponseHeaderTimeout, 
"ResponseHeaderTimeout should be custom value")
        })
 
        t.Run("partial custom settings use defaults for unset values", func(t 
*testing.T) {

Reply via email to