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 {
