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 dfd95956e03c752feda3411eddba52c3a16ea5bc
Author: Cole Greer <[email protected]>
AuthorDate: Tue Jun 2 12:37:34 2026 -0700

    Add request timeout and wrap transport errors in gremlin-javascript
    
    - Add a requestTimeout option bounding time-to-first-byte (cleared once 
response
      headers arrive so slow streams are unaffected); reject with a 
ResponseError
      when the server never responds (tinkerpop-uvl).
    - Wrap low-level fetch/transport errors in a Gremlin-aware ResponseError 
with an
      actionable message, preserving the original error as the cause 
(tinkerpop-1fn).
    - Tighten behavioral test assertions.
---
 .../gremlin-javascript/lib/driver/connection.ts    | 57 +++++++++++++++++++---
 .../test/integration/client-behavior-tests.js      | 24 +++++++--
 2 files changed, 70 insertions(+), 11 deletions(-)

diff --git a/gremlin-js/gremlin-javascript/lib/driver/connection.ts 
b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
index ef57960b73..a47b815a19 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
@@ -62,6 +62,8 @@ export type ConnectionOptions = {
   enableUserAgentOnConnect?: boolean;
   agent?: Agent;
   interceptors?: RequestInterceptor | RequestInterceptor[];
+  /** Maximum time in milliseconds to wait for a server response before 
aborting the request. Undefined means no timeout. */
+  requestTimeout?: number;
 };
 
 /**
@@ -257,17 +259,58 @@ export default class Connection extends EventEmitter {
       }
     }
 
-    return fetch(httpRequest.url, {
-      method: httpRequest.method,
-      headers: httpRequest.headers,
-      body: httpRequest.body,
-      signal,
-    });
+    let effectiveSignal = signal;
+    const timeoutMs = this.options.requestTimeout;
+    let timeoutId: ReturnType<typeof setTimeout> | undefined;
+    let timeoutController: AbortController | undefined;
+
+    if (timeoutMs !== undefined) {
+      timeoutController = new AbortController();
+      timeoutId = setTimeout(() => timeoutController!.abort(new 
DOMException('TimeoutError', 'TimeoutError')), timeoutMs);
+      effectiveSignal = signal ? AbortSignal.any([signal, 
timeoutController.signal]) : timeoutController.signal;
+    }
+
+    try {
+      const response = await fetch(httpRequest.url, {
+        method: httpRequest.method,
+        headers: httpRequest.headers,
+        body: httpRequest.body,
+        signal: effectiveSignal,
+      });
+      return response;
+    } catch (err: any) {
+      if (timeoutController?.signal.aborted) {
+        const e = new ResponseError(
+          `Request timed out after ${timeoutMs}ms - the server did not respond 
in time`,
+          { code: 598, message: `Request timeout: ${timeoutMs}ms exceeded` },
+        );
+        e.cause = err;
+        throw e;
+      }
+      const e = new ResponseError(
+        'Connection to server closed unexpectedly. Ensure that the server is 
still reachable and the connection has not been closed by the server or a 
network device.',
+        { code: 599, message: err.message || 'Connection failed' },
+      );
+      e.cause = err;
+      throw e;
+    } finally {
+      if (timeoutId !== undefined) clearTimeout(timeoutId);
+    }
   }
 
   async #handleResponse(response: Response) {
+    let buffer: Buffer;
+    try {
+      buffer = Buffer.from(await response.arrayBuffer());
+    } catch (err: any) {
+      const e = new ResponseError(
+        'Connection to server closed unexpectedly. Ensure that the server is 
still reachable and the connection has not been closed by the server or a 
network device.',
+        { code: 599, message: err.message || 'Connection failed' },
+      );
+      e.cause = err;
+      throw e;
+    }
     const contentType = response.headers.get("Content-Type");
-    const buffer = Buffer.from(await response.arrayBuffer());
     const reader = this.#getReaderForContentType(contentType);
 
     if (!response.ok) {
diff --git 
a/gremlin-js/gremlin-javascript/test/integration/client-behavior-tests.js 
b/gremlin-js/gremlin-javascript/test/integration/client-behavior-tests.js
index 98f6537aa0..53be66adbc 100644
--- a/gremlin-js/gremlin-javascript/test/integration/client-behavior-tests.js
+++ b/gremlin-js/gremlin-javascript/test/integration/client-behavior-tests.js
@@ -67,7 +67,11 @@ describe('Client Behavior', function () {
   });
 
   it('should handle connection close before response and recover', async 
function () {
-    await assert.rejects(client.submit(GREMLIN_CLOSE_CONNECTION), /fetch 
failed/);
+    await assert.rejects(client.submit(GREMLIN_CLOSE_CONNECTION), (err) => {
+      assert.strictEqual(err.name, 'ResponseError');
+      assert.match(err.message, /Connection to server closed unexpectedly/);
+      return true;
+    });
     const result = await client.submit(GREMLIN_SINGLE_VERTEX);
     assert.strictEqual(result.length, 1);
   });
@@ -93,7 +97,11 @@ describe('Client Behavior', function () {
   });
 
   it('should handle partial content close and recover', async function () {
-    await assert.rejects(client.submit(GREMLIN_PARTIAL_CONTENT_CLOSE), 
/terminated/);
+    await assert.rejects(client.submit(GREMLIN_PARTIAL_CONTENT_CLOSE), (err) 
=> {
+      assert.strictEqual(err.name, 'ResponseError');
+      assert.match(err.message, /Connection to server closed unexpectedly/);
+      return true;
+    });
     const result = await client.submit(GREMLIN_SINGLE_VERTEX);
     assert.strictEqual(result.length, 1);
   });
@@ -117,10 +125,18 @@ describe('Client Behavior', function () {
     assert.ok(result.length > 0);
   });
 
-  it.skip('should timeout when server never responds - JS driver lacks 
client-side idle timeout', async function () {
+  it('should timeout when server never responds', async function () {
+    this.timeout(5000);
     const shortTimeoutClient = createClient({ requestTimeout: 1000 });
     try {
-      await assert.rejects(shortTimeoutClient.submit(GREMLIN_NO_RESPONSE));
+      await assert.rejects(
+        shortTimeoutClient.submit(GREMLIN_NO_RESPONSE),
+        (err) => {
+          assert.match(err.message, /timed out/i);
+          assert.strictEqual(err.name, 'ResponseError');
+          return true;
+        },
+      );
       const result = await shortTimeoutClient.submit(GREMLIN_SINGLE_VERTEX);
       assert.strictEqual(result.length, 1);
     } finally {

Reply via email to