[ https://issues.apache.org/jira/browse/TINKERPOP-3160?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17954228#comment-17954228 ]
Mark Pigge commented on TINKERPOP-3160: --------------------------------------- Hi [~andreac] , Thanks for your quick response. In our case we didn't use in the browser, but only from the back-end services in NodeJS. There is limitation in the javascript driver related to custom auth headers that custom auth header will be not added when using a global websocket: [https://github.com/apache/tinkerpop/blob/master/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/connection.ts#L196] As a solution I can imagine that you have a default websocket for browser and NodeJS, but that you can override this with your own WebSocket factory, and that also the custom auth headers will be applied in this case. Also I have the question: why is the NodeJS 22+ WebSocket implementation incompatible when communicating over TLS with Neptune? If I connect locally to TinkerPop server then it will work flawlessly I build the following workaround for aws presigned URLs this: Websocket factory wrapper {code:java} import { Logger } from '@nestjs/common'; import { URL } from 'url'; import { WebSocket } from 'ws'; // Fix: gremlin network errors on Node version >= 22 // // Use WebSocket implementation of library instead of Node WebSocket for gremlin connections only // // See https://medium.com/@python-javascript-php-html-css/resolving-gremlin-network-errors-after-upgrading-to-node-js-23-3591c0e45caaexport const WebSocketOriginal = (globalThis as any).WebSocket; export class WebSocketFactory { protected static readonly logger = new Logger(this.constructor.name); private static readonly headerStore = new Map< string, Record<string, string> >(); private static initialized = false; constructor(address: string, protocols?: string | string[], options?: any) { const url = new URL(address); const query = url.searchParams; const useGremlinWebSocket = query.get('websocketFactory') === 'gremlin'; if (useGremlinWebSocket) { const connectionId = query.get('connectionId'); query.delete('websocketFactory'); query.delete('connectionId'); const strippedUrl = url.toString(); let headers; if (connectionId) { headers = WebSocketFactory.getHeaders(connectionId); WebSocketFactory.logger.debug( `[WebSocketFactory] Using custom WebSocket with connectionId=[${connectionId}] and headers=[${JSON.stringify( headers, null, 2 )}]` ); WebSocketFactory.deleteHeaders(connectionId); } else { WebSocketFactory.logger.debug( '[WebSocketFactory] Using custom WebSocket without header injection' ); } return new WebSocket(strippedUrl, protocols, { ...(options || {}), headers: { ...(options?.headers || {}), ...(headers || {}), }, }); } else { return new WebSocketOriginal(url, protocols, options); } } static injectHeaders( connectionId: string, headers: Record<string, string>, ttlMs = 60_000 ) { WebSocketFactory.headerStore.set(connectionId, headers); setTimeout(() => { WebSocketFactory.logger.debug( `[WebSocketFactory] Headers TTL expired for connectionId=${connectionId}` ); WebSocketFactory.headerStore.delete(connectionId); }, ttlMs); } private static getHeaders(connectionId): Record<string, string> { return WebSocketFactory.headerStore.get(connectionId); } private static deleteHeaders(connectionId: string) { WebSocketFactory.headerStore.delete(connectionId); } static init() { if (!this.initialized) { (globalThis as any).WebSocket = WebSocketFactory; this.initialized = true; } } } {code} Instantiating AWS Neptune connection {code:java} private async getConnection(): Promise<driver.RemoteConnection> { if (!this.connection) { if (this.config.isIamAuthenticationMode) { const credentialProvider = fromNodeProviderChain(); let credentials = await credentialProvider(); if (this.config.hasIamRole) { const sts = new STS({ credentials }); const role = await sts.assumeRole({ RoleArn: this.config.iamRole, RoleSessionName: 'neptune-connection', }); credentials = { secretAccessKey: role.Credentials.SecretAccessKey, accessKeyId: role.Credentials.AccessKeyId, sessionToken: role.Credentials.SessionToken, }; } const signer = new SignatureV4({ credentials, region: this.config.region, sha256: Sha256, service: NEPTUNE_SERVICE, }); const uri = parseUrl(this.config.gremlinGraphDbBaseUrl); const signedRequest = await signer.sign(<HttpRequest>{ method: 'GET', ...uri, query: {}, headers: { host: uri.hostname, }, }); const connectionId = uuidv4(); WebSocketFactory.init(); WebSocketFactory.injectHeaders(connectionId, signedRequest.headers); this.connection = new driver.DriverRemoteConnection( `${this.config.gremlinGraphDbBaseUrl}?websocketFactory=gremlin&connectionId=${connectionId}` ); } else { this.connection = new driver.DriverRemoteConnection( this.config.gremlinGraphDbBaseUrl, {} ); } } return this.connection; } {code} I think this will give you some insight in the issue. > Node.js 22+: Gremlin Fails with network error and HTTP 101 Status Due to > WebSocket Limitation in undici > ------------------------------------------------------------------------------------------------------- > > Key: TINKERPOP-3160 > URL: https://issues.apache.org/jira/browse/TINKERPOP-3160 > Project: TinkerPop > Issue Type: Bug > Components: javascript > Affects Versions: 3.7.3 > Reporter: Mark Pigge > Priority: Major > > *📌 Overview:* > After upgrading to {*}Node.js 22 or newer{*}, applications using the *Gremlin > driver* encounter {{network error}} failures when trying to connect to graph > database AWS Neptune. These failures are caused by the removal of built-in > WebSocket support in Node.js and reliance on {{{}undici{}}}, which does *not > support custom headers* or handle the WebSocket handshake properly in this > context. > ---- > *❌ Symptoms:* > * {{Gremlin}} client fails to connect > * Network error during connection > * HTTP 101 Switching Protocols error with no upgrade > * Headers like {{Authorization}} or {{Host}} are missing or ignored > * Stack trace shows underlying issues in WebSocket handshake > > ---- > *🎯 Root Cause:* > * Node.js 22+ no longer includes a global {{WebSocket}} implementation > * The fallback (via {{{}undici{}}}) *does not support* custom headers > * This breaks clients that rely on authenticated WebSocket connections (like > Gremlin) > * Result: WebSocket upgrade fails with a 101 status and unresolved {{network > error}} > > ---- > *✅ Workaround:* > You can override the default WebSocket globally with the {{ws}} library, > which supports custom headers and proper handshake behavior. > import \{ WebSocket as WS } from 'ws'; > (globalThis as any).WebSocket = WS; > > This should be done *early in your application startup* before any Gremlin > connections are created. > > ---- > *📄 Example Fix for Gremlin Driver:* > import \{ WebSocket as WS } from 'ws'; > (globalThis as any).WebSocket = WS; > import \{ DriverRemoteConnection } from 'gremlin'; > const connection = new DriverRemoteConnection( > 'wss://your-gremlin-endpoint:8182/gremlin', \{ > // headers like Authorization must now be passed via a custom WebSocket > } > ); > > > ---- > *📚 References:* > * Node.js change: [{{undici}} removed WebSocket > support|https://github.com/nodejs/undici/discussions/3836] > * Community write-up: [Resolving Gremlin network errors after upgrading to > Node.js > 23|https://medium.com/@python-javascript-php-html-css/resolving-gremlin-network-errors-after-upgrading-to-node-js-23-3591c0e45caa] -- This message was sent by Atlassian Jira (v8.20.10#820010)