From 296658d3bc086bb839365aba9c65bf08de306aa2 Mon Sep 17 00:00:00 2001 From: Anand Suresh Date: Mon, 10 Oct 2016 15:11:48 -0700 Subject: [PATCH 01/11] Fix spdy-transport dependency Use anandsuresh/spdy-transport instead of the public npm package until required changes have been merged and released on the public branch. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3296309..6f1aecb2 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "http-deceiver": "^1.2.7", "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" + "spdy-transport": "anandsuresh/spdy-transport#voxer" }, "devDependencies": { "istanbul": "^0.4.5", From 51fc2db2c235dde6313082f08be14a0f5e01903f Mon Sep 17 00:00:00 2001 From: Anand Suresh Date: Fri, 7 Oct 2016 17:18:38 -0700 Subject: [PATCH 02/11] Fixes agent to handle incoming responses correctly (#3) Currently, the node-spdy agent seems to emit an "aborted" event on every incoming response because the `ending` flag isn't set for agent streams. This commit sets the `ending` flag to true for agent streams. Fixes #281 --- lib/spdy/handle.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/spdy/handle.js b/lib/spdy/handle.js index 1576e175..e786c816 100644 --- a/lib/spdy/handle.js +++ b/lib/spdy/handle.js @@ -28,9 +28,18 @@ function Handle (options, stream, socket) { }) if (!state.stream) { +<<<<<<< HEAD this.on('stream', function (stream) { state.stream = stream }) +======= + this.on('stream', function(stream) { + state.stream = stream; + stream.on('end', function() { + state.ending = true; + }); + }); +>>>>>>> Fixes agent to handle incoming responses correctly (#3) } } util.inherits(Handle, thing) From 3562cc9e2912895fb8f0eccd39e3c0d7c8950a0f Mon Sep 17 00:00:00 2001 From: Anand Suresh Date: Mon, 7 Nov 2016 09:58:30 -0800 Subject: [PATCH 03/11] Add checks for GOAWAY conditions on the agent (#6) Under certain conditions, it is possible for the code to send frames AFTER a remote server has sent a GOAWAY frame. This commit fixes the problem. --- lib/spdy/handle.js | 12 +++--------- lib/spdy/socket.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/spdy/handle.js b/lib/spdy/handle.js index e786c816..3ac9bd3b 100644 --- a/lib/spdy/handle.js +++ b/lib/spdy/handle.js @@ -28,18 +28,12 @@ function Handle (options, stream, socket) { }) if (!state.stream) { -<<<<<<< HEAD this.on('stream', function (stream) { state.stream = stream + stream.on('end', function () { + state.ending = true + }) }) -======= - this.on('stream', function(stream) { - state.stream = stream; - stream.on('end', function() { - state.ending = true; - }); - }); ->>>>>>> Fixes agent to handle incoming responses correctly (#3) } } util.inherits(Handle, thing) diff --git a/lib/spdy/socket.js b/lib/spdy/socket.js index 78641a69..0073f96a 100644 --- a/lib/spdy/socket.js +++ b/lib/spdy/socket.js @@ -12,6 +12,7 @@ function Socket (parent, options) { state.parent = parent +<<<<<<< HEAD this.servername = parent.servername this.npnProtocol = parent.npnProtocol this.alpnProtocol = parent.alpnProtocol @@ -19,6 +20,15 @@ function Socket (parent, options) { this.authorizationError = parent.authorizationError this.encrypted = true this.allowHalfOpen = true +======= + this.servername = parent.servername; + this.npnProtocol = parent.npnProtocol; + this.alpnProtocol = parent.alpnProtocol; + this.authorized = parent.authorized; + this.authorizationError = parent.authorizationError; + this.encrypted = true; + this.allowHalfOpen = true; +>>>>>>> Add checks for GOAWAY conditions on the agent (#6) } util.inherits(Socket, net.Socket) From bbf0f89163fcdc44d6125098d7e4875d24fe4a57 Mon Sep 17 00:00:00 2001 From: Anand Suresh Date: Mon, 7 Nov 2016 15:38:25 -0800 Subject: [PATCH 04/11] Handle connection-level errors correctly on agents (#7) The old code was missing a handler for the connection-level "error" events. This commit adds an error-handler that re-emits the "error" on the agent that user-code can handle as needed. --- lib/spdy/agent.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/spdy/agent.js b/lib/spdy/agent.js index 8ce934f2..1ee2b856 100644 --- a/lib/spdy/agent.js +++ b/lib/spdy/agent.js @@ -94,6 +94,7 @@ proto._getCreateSocket = function _getCreateSocket () { } proto._connect = function _connect (options, callback) { + var self = this var state = this._spdyState var protocols = state.options.protocols || [ @@ -158,6 +159,10 @@ proto._connect = function _connect (options, callback) { return } + connection.on('error', function (err) { + self.emit('error', err) + }) + if (state.options['x-forwarded-for'] !== undefined) { connection.sendXForwardedFor(state.options['x-forwarded-for']) } From 4b61e380c834312b6ff86fcfcad7303871a38198 Mon Sep 17 00:00:00 2001 From: Anand Suresh Date: Thu, 29 Mar 2018 15:49:20 -0700 Subject: [PATCH 05/11] fixed bad merge conflict --- lib/spdy/socket.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/spdy/socket.js b/lib/spdy/socket.js index 0073f96a..78641a69 100644 --- a/lib/spdy/socket.js +++ b/lib/spdy/socket.js @@ -12,7 +12,6 @@ function Socket (parent, options) { state.parent = parent -<<<<<<< HEAD this.servername = parent.servername this.npnProtocol = parent.npnProtocol this.alpnProtocol = parent.alpnProtocol @@ -20,15 +19,6 @@ function Socket (parent, options) { this.authorizationError = parent.authorizationError this.encrypted = true this.allowHalfOpen = true -======= - this.servername = parent.servername; - this.npnProtocol = parent.npnProtocol; - this.alpnProtocol = parent.alpnProtocol; - this.authorized = parent.authorized; - this.authorizationError = parent.authorizationError; - this.encrypted = true; - this.allowHalfOpen = true; ->>>>>>> Add checks for GOAWAY conditions on the agent (#6) } util.inherits(Socket, net.Socket) From 8f1b972eb018eebb89b0ea9a56e73c262e7bb87f Mon Sep 17 00:00:00 2001 From: Anand Suresh Date: Thu, 29 Mar 2018 15:59:40 -0700 Subject: [PATCH 06/11] fixes for windowBits in node v8 (#8) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f1aecb2..64aaaa23 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "http-deceiver": "^1.2.7", "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "anandsuresh/spdy-transport#voxer" + "spdy-transport": "anandsuresh/spdy-transport#fix/windowBits" }, "devDependencies": { "istanbul": "^0.4.5", From 85f2fd2b33e9c4c74d16063e8febafb80075549a Mon Sep 17 00:00:00 2001 From: Anand Suresh Date: Thu, 29 Mar 2018 16:28:24 -0700 Subject: [PATCH 07/11] fix dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64aaaa23..6f1aecb2 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "http-deceiver": "^1.2.7", "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "anandsuresh/spdy-transport#fix/windowBits" + "spdy-transport": "anandsuresh/spdy-transport#voxer" }, "devDependencies": { "istanbul": "^0.4.5", From 22c5cc29f017a7ce8f0eb0ebecffc3c2788d6faf Mon Sep 17 00:00:00 2001 From: Julian Gautier Date: Wed, 5 Sep 2018 14:32:03 -0700 Subject: [PATCH 08/11] update package.json to point to Voxer repo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f1aecb2..4e938b32 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "http-deceiver": "^1.2.7", "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "anandsuresh/spdy-transport#voxer" + "spdy-transport": "Voxer/spdy-transport#voxer" }, "devDependencies": { "istanbul": "^0.4.5", From b2dfe920c76dcedb6d19547e0a515f87defd22da Mon Sep 17 00:00:00 2001 From: Patrick Kokou Date: Thu, 14 May 2026 02:44:37 -0700 Subject: [PATCH 09/11] Replace http-deceiver dependency with vendored version in deceiver.js and update handle.js to use the new module. Remove http-deceiver from package.json dependencies. --- lib/spdy/deceiver.js | 270 +++++++++++++++++++++++++++++++++++++++++++ lib/spdy/handle.js | 2 +- package.json | 1 - 3 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 lib/spdy/deceiver.js diff --git a/lib/spdy/deceiver.js b/lib/spdy/deceiver.js new file mode 100644 index 00000000..86f212cf --- /dev/null +++ b/lib/spdy/deceiver.js @@ -0,0 +1,270 @@ +// Vendored from http-deceiver@1.2.7 (indutny/http-deceiver, MIT). +// Two changes from upstream: +// 1. The `process.binding('http_parser')` block is wrapped in try/catch +// and falls back to `mode = 'unsupported'`. The internal binding was +// removed in Node 16+ ("No such module: http_parser"); upstream is +// abandoned. The fallback uses string event names instead of integer +// kOn* indices and works for spdy's purposes. +// 2. `new Buffer(0)` -> `Buffer.alloc(0)` (the legacy form is removed in +// modern Node). + +var assert = require('assert') + +var Buffer = require('buffer').Buffer + +// Node.js version +var mode = /^v0\.8\./.test(process.version) ? 'rusty' + : /^v0\.(9|10)\./.test(process.version) ? 'old' + : /^v0\.12\./.test(process.version) ? 'normal' + : 'modern' + +var HTTPParser + +var methods +var reverseMethods + +var kOnHeaders +var kOnHeadersComplete +var kOnMessageComplete +var kOnBody + +if (mode === 'normal' || mode === 'modern') { + try { + HTTPParser = process.binding('http_parser').HTTPParser + methods = HTTPParser.methods + + // v6 + if (!methods) { + methods = process.binding('http_parser').methods + } + + reverseMethods = {} + + methods.forEach(function (method, index) { + reverseMethods[method] = index + }) + + kOnHeaders = HTTPParser.kOnHeaders | 0 + kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0 + kOnMessageComplete = HTTPParser.kOnMessageComplete | 0 + kOnBody = HTTPParser.kOnBody | 0 + } catch (e) { + mode = 'unsupported' + } +} +if (mode !== 'normal' && mode !== 'modern') { + kOnHeaders = 'onHeaders' + kOnHeadersComplete = 'onHeadersComplete' + kOnMessageComplete = 'onMessageComplete' + kOnBody = 'onBody' +} + +function Deceiver (socket, options) { + this.socket = socket + this.options = options || {} + this.isClient = this.options.isClient +} +module.exports = Deceiver + +Deceiver.create = function create (stream, options) { + return new Deceiver(stream, options) +} + +Deceiver.prototype._toHeaderList = function _toHeaderList (object) { + var out = [] + var keys = Object.keys(object) + + for (var i = 0; i < keys.length; i++) { + out.push(keys[i], object[keys[i]]) + } + + return out +} + +Deceiver.prototype._isUpgrade = function _isUpgrade (request) { + return request.method === 'CONNECT' || + request.headers.upgrade || + (request.headers.connection && + /(^|\W)upgrade(\W|$)/i.test(request.headers.connection)) +} + +// TODO(indutny): support CONNECT +if (mode === 'modern') { + /* + function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, + url, statusCode, statusMessage, upgrade, + shouldKeepAlive) { + */ + Deceiver.prototype.emitRequest = function emitRequest (request) { + var parser = this.socket.parser + assert(parser, 'No parser present') + + parser.execute = null + + var self = this + var method = reverseMethods[request.method] + parser.execute = function execute () { + self._skipExecute(this) + this[kOnHeadersComplete](1, + 1, + self._toHeaderList(request.headers), + method, + request.path, + 0, + '', + self._isUpgrade(request), + true) + return 0 + } + + this._emitEmpty() + } + + Deceiver.prototype.emitResponse = function emitResponse (response) { + var parser = this.socket.parser + assert(parser, 'No parser present') + + parser.execute = null + + var self = this + parser.execute = function execute () { + self._skipExecute(this) + this[kOnHeadersComplete](1, + 1, + self._toHeaderList(response.headers), + response.path, + response.code, + response.status, + response.reason || '', + self._isUpgrade(response), + true) + return 0 + } + + this._emitEmpty() + } +} else { + /* + `function parserOnHeadersComplete(info) {` + + info = { .versionMajor, .versionMinor, .url, .headers, .method, + .statusCode, .statusMessage, .upgrade, .shouldKeepAlive } + */ + Deceiver.prototype.emitRequest = function emitRequest (request) { + var parser = this.socket.parser + assert(parser, 'No parser present') + + var method = request.method + if (reverseMethods) { + method = reverseMethods[method] + } + + var info = { + versionMajor: 1, + versionMinor: 1, + url: request.path, + headers: this._toHeaderList(request.headers), + method: method, + statusCode: 0, + statusMessage: '', + upgrade: this._isUpgrade(request), + shouldKeepAlive: true + } + + var self = this + parser.execute = function execute () { + self._skipExecute(this) + this[kOnHeadersComplete](info) + return 0 + } + + this._emitEmpty() + } + + Deceiver.prototype.emitResponse = function emitResponse (response) { + var parser = this.socket.parser + assert(parser, 'No parser present') + + var info = { + versionMajor: 1, + versionMinor: 1, + url: response.path, + headers: this._toHeaderList(response.headers), + method: false, + statusCode: response.status, + statusMessage: response.reason || '', + upgrade: this._isUpgrade(response), + shouldKeepAlive: true + } + + var self = this + parser.execute = function execute () { + self._skipExecute(this) + this[kOnHeadersComplete](info) + return 0 + } + + this._emitEmpty() + } +} + +Deceiver.prototype._skipExecute = function _skipExecute (parser) { + var self = this + var oldExecute = parser.constructor.prototype.execute + var oldFinish = parser.constructor.prototype.finish + + parser.execute = null + parser.finish = null + + parser.execute = function execute (buffer, start, len) { + // Parser reuse + if (this.socket !== self.socket) { + this.execute = oldExecute + this.finish = oldFinish + return this.execute(buffer, start, len) + } + + if (start !== undefined) { + buffer = buffer.slice(start, start + len) + } + self.emitBody(buffer) + return len + } + + parser.finish = function finish () { + // Parser reuse + if (this.socket !== self.socket) { + this.execute = oldExecute + this.finish = oldFinish + return this.finish() + } + + this.execute = oldExecute + this.finish = oldFinish + self.emitMessageComplete() + } +} + +Deceiver.prototype.emitBody = function emitBody (buffer) { + var parser = this.socket.parser + assert(parser, 'No parser present') + + parser[kOnBody](buffer, 0, buffer.length) +} + +Deceiver.prototype._emitEmpty = function _emitEmpty () { + // Emit data to force out handling of UPGRADE + var empty = Buffer.alloc(0) + if (this.socket.ondata) { + this.socket.ondata(empty, 0, 0) + } else { + this.socket.emit('data', empty) + } +} + +Deceiver.prototype.emitMessageComplete = function emitMessageComplete () { + var parser = this.socket.parser + assert(parser, 'No parser present') + + parser[kOnMessageComplete]() +} diff --git a/lib/spdy/handle.js b/lib/spdy/handle.js index 3ac9bd3b..903daa69 100644 --- a/lib/spdy/handle.js +++ b/lib/spdy/handle.js @@ -2,7 +2,7 @@ var assert = require('assert') var thing = require('handle-thing') -var httpDeceiver = require('http-deceiver') +var httpDeceiver = require('./deceiver') var util = require('util') function Handle (options, stream, socket) { diff --git a/package.json b/package.json index 4e938b32..691fd5f0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "dependencies": { "debug": "^2.6.8", "handle-thing": "^1.2.5", - "http-deceiver": "^1.2.7", "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", "spdy-transport": "Voxer/spdy-transport#voxer" From 5c3d7ca42eefd5dbd39be28d898f0255864e4fda Mon Sep 17 00:00:00 2001 From: Patrick Kokou Date: Fri, 15 May 2026 15:48:25 -0700 Subject: [PATCH 10/11] deceiver: load HTTPParser from node:_http_common on Node 16+ `process.binding('http_parser')` was removed in Node 16. The previous fallback set `mode = 'unsupported'` and used string callback names (`'onHeadersComplete'` etc.), but the parser instances on modern Node expose those callbacks on numeric slots only, so any incoming request crashed the process with: TypeError: this[kOnHeadersComplete] is not a function at HTTPParser.execute (.../spdy/lib/spdy/deceiver.js:177:31) Hit on gcp1-prod-0064 (NR processes nr128-nr135). Load `HTTPParser` from `require('node:_http_common')` (with `require('_http_common')` as a tier-2 fallback) and the methods list from `require('http').METHODS` when `HTTPParser.methods` is missing. This keeps the `mode === 'modern'` numeric-slot path which matches how the modern parser is wired. Smoke-tested on Node v24.11.1: spdy.createServer({plain:true}) now parses an HTTP/1.1 request and returns 200 OK; before this change the same request triggered the uncaughtException above. Co-authored-by: Cursor --- lib/spdy/deceiver.js | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/spdy/deceiver.js b/lib/spdy/deceiver.js index 86f212cf..5c65c5f1 100644 --- a/lib/spdy/deceiver.js +++ b/lib/spdy/deceiver.js @@ -1,10 +1,14 @@ // Vendored from http-deceiver@1.2.7 (indutny/http-deceiver, MIT). -// Two changes from upstream: -// 1. The `process.binding('http_parser')` block is wrapped in try/catch -// and falls back to `mode = 'unsupported'`. The internal binding was -// removed in Node 16+ ("No such module: http_parser"); upstream is -// abandoned. The fallback uses string event names instead of integer -// kOn* indices and works for spdy's purposes. +// Voxer changes from upstream: +// 1. `process.binding('http_parser')` was removed in Node 16+ +// ("No such module: http_parser"). When that fails we now load +// `HTTPParser` from `node:_http_common` (Node 16+) and the +// methods list from `require('http').METHODS`, keeping the +// `mode === 'modern'` numeric kOn* path. The previous fallback +// to `mode = 'unsupported'` used string event names that the +// modern parser does not expose, so `parser[kOnHeadersComplete]` +// was undefined and any request crashed the process with +// `TypeError: this[kOnHeadersComplete] is not a function`. // 2. `new Buffer(0)` -> `Buffer.alloc(0)` (the legacy form is removed in // modern Node). @@ -30,12 +34,29 @@ var kOnBody if (mode === 'normal' || mode === 'modern') { try { - HTTPParser = process.binding('http_parser').HTTPParser + try { + HTTPParser = process.binding('http_parser').HTTPParser + } catch (e) { + // Node 16+: process.binding('http_parser') is gone. The same + // HTTPParser constructor (with kOn* slot indices) is exported + // from the internal _http_common module. + try { + HTTPParser = require('node:_http_common').HTTPParser + } catch (e2) { + HTTPParser = require('_http_common').HTTPParser + } + } methods = HTTPParser.methods // v6 if (!methods) { - methods = process.binding('http_parser').methods + try { + methods = process.binding('http_parser').methods + } catch (e) { + // Node 16+: HTTPParser does not carry .methods anymore; + // http.METHODS is the public, equivalent list. + methods = require('http').METHODS + } } reverseMethods = {} From ca3e4d3292ab01baf1c63d580cf76e1b6f8180f6 Mon Sep 17 00:00:00 2001 From: Patrick Kokou Date: Fri, 15 May 2026 19:47:01 -0700 Subject: [PATCH 11/11] deceiver+handle-thing: fix request method lookup and -4095 EOF crash on Node 16+ Two follow-ups to the earlier deceiver fix; both surfaced when bringing up gcp2-dev03-00 on Node 24. 1. handle-thing@^1.2.5 calls socket._handle.onread with the old (nread, buffer) signature. On Node 11.1+, the handle's onread is actually node:internal/stream_base_commons.onStreamRead(arrayBuffer), which reads nread from streamBaseState[kReadBytesOrError]. Passing UV_EOF (-4095) as the arrayBuffer arg made onStreamRead try `new FastBuffer(-4095, ...)` and crash: RangeError: Invalid typed array length: -4095 at new FastBuffer (node:internal/buffer:956:1) at Handle.onStreamRead [as onread] (node:internal/stream_base_commons:188:19) at Stream. (.../handle-thing/lib/handle.js:120:12) handle-thing@2.0.1 (spdy-http2/handle-thing#13) translates the call: it sets streamBaseState[kReadBytesOrError] first and then invokes onread with just the buffer. It also adds .writev which modern net.Socket needs. Bump the dependency. 2. The previous deceiver fallback used `require('http').METHODS` for the methods list. http.METHODS is alphabetically sorted, but the parser's internal index is the llhttp/http_parser order (DELETE, GET, HEAD, POST, ...). Sending the alphabetical index made Node interpret POST as ACL etc. Use `_http_common.methods` (or `node:_http_common.methods`), which is the real parser-indexed list. Smoke-tested end-to-end on Node v24.11.1: spdy.createServer with TLS + h2 alpn, hit with the built-in http2 client, POST /, 11-byte body, server sees method=POST, body bytes flow through handle-thing's data path, EOF path completes without the -4095 crash, client receives 200 OK with the response body. Co-authored-by: Cursor --- lib/spdy/deceiver.js | 13 ++++++++++--- package.json | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/spdy/deceiver.js b/lib/spdy/deceiver.js index 5c65c5f1..df8a0cdc 100644 --- a/lib/spdy/deceiver.js +++ b/lib/spdy/deceiver.js @@ -53,9 +53,16 @@ if (mode === 'normal' || mode === 'modern') { try { methods = process.binding('http_parser').methods } catch (e) { - // Node 16+: HTTPParser does not carry .methods anymore; - // http.METHODS is the public, equivalent list. - methods = require('http').METHODS + // Node 16+: HTTPParser does not carry .methods anymore. + // _http_common exports the parser-indexed list (DELETE, GET, + // HEAD, POST, ...). http.METHODS is alphabetically sorted, so + // using it here would shift indices and the deceiver would tell + // Node the wrong method (e.g. POST -> ACL). + try { + methods = require('node:_http_common').methods + } catch (e2) { + methods = require('_http_common').methods + } } } diff --git a/package.json b/package.json index 691fd5f0..a9163526 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ ], "dependencies": { "debug": "^2.6.8", - "handle-thing": "^1.2.5", + "handle-thing": "^2.0.1", "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", "spdy-transport": "Voxer/spdy-transport#voxer"