From 01fb356d01e6da682c2367c3cbd59387f51619af Mon Sep 17 00:00:00 2001 From: Ratchanan Srirattanamet Date: Sat, 9 Feb 2019 01:40:17 +0700 Subject: [PATCH] server: add option enableChunkedEncoding Using res.end() to write result instead of res.write() followed by res.end() will cause node not to use chunked encoding and include Content-Length header. (See nodejs/node#26005) This small distinction is important with some client. For example, Windows 10's MDM enrollment system will not accept chunked response (https://docs.microsoft.com/en-us/windows/client-management/mdm/on-premise-authentication-device-enrollment). This might improve compatibility with some other clients, too. However, there's a small chance that some client may expect the response to be chunked. So, I put this behind an option which defaults to enabled and can be disabled if needed. --- Readme.md | 1 + lib/server.js | 37 ++++++++++----- test/server-options-test.js | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 11 deletions(-) diff --git a/Readme.md b/Readme.md index e806aaf3c..0671d53f5 100644 --- a/Readme.md +++ b/Readme.md @@ -219,6 +219,7 @@ Server options include the below: - `ca`: An array of strings or Buffers of trusted certificates in PEM format. If this is omitted several well known "root" CAs will be used, like VeriSign. These are used to authorize connections. - `crl` : Either a string or list of strings of PEM encoded CRLs (Certificate Revocation List) - `ciphers`: A string describing the ciphers to use or exclude, separated by :. The default cipher suite is: +- `enableChunkedEncoding`: A boolean for controlling chunked transfer encoding in response. Some client (such as Windows 10's MDM enrollment SOAP client) is sensitive to transfer-encoding mode and can't accept chunked response. This option let user disable chunked transfer encoding for such a client. Default to `true` for backward compatibility. ``` javascript var xml = require('fs').readFileSync('myservice.wsdl', 'utf8'); diff --git a/lib/server.js b/lib/server.js index a5098d073..cda278315 100644 --- a/lib/server.js +++ b/lib/server.js @@ -40,6 +40,8 @@ var Server = function (server, path, services, wsdl, options) { this.suppressStack = options && options.suppressStack; this.returnFault = options && options.returnFault; this.onewayOptions = options && options.oneWay || {}; + this.enableChunkedEncoding = + options.enableChunkedEncoding === undefined ? true : !!options.enableChunkedEncoding; if (path[path.length - 1] !== '/') path += '/'; @@ -144,11 +146,7 @@ Server.prototype._processRequestXml = function (req, res, xml) { self.log("received", xml); } self._process(xml, req, function (result, statusCode) { - if (statusCode) { - res.statusCode = statusCode; - } - res.write(result); - res.end(); + self._sendHttpResponse(res, statusCode, result); if (typeof self.log === 'function') { self.log("replied", result); } @@ -156,18 +154,14 @@ Server.prototype._processRequestXml = function (req, res, xml) { } catch (err) { if (err.Fault !== undefined) { return self._sendError(err.Fault, function (result, statusCode) { - res.statusCode = statusCode || 500; - res.write(result); - res.end(); + self._sendHttpResponse(res, statusCode || 500, result); if (typeof self.log === 'function') { self.log("error", err); } }, new Date().toISOString()); } else { error = err.stack ? (self.suppressStack === true ? err.message : err.stack) : err; - res.statusCode = 500; - res.write(error); - res.end(); + self._sendHttpResponse(res, /* statusCode */ 500, error); if (typeof self.log === 'function') { self.log("error", error); } @@ -517,4 +511,25 @@ Server.prototype._sendError = function (soapFault, callback, includeTimestamp) { return callback(self._envelope(fault, '', includeTimestamp), statusCode); }; +Server.prototype._sendHttpResponse = function (res, statusCode, result) { + if (statusCode) { + res.statusCode = statusCode; + } + + /* + * Calling res.write(result) follow by res.end() will cause Node.js to use + * chunked encoding, while calling res.end(result) directly will cause + * Node.js to calculate and send Content-Length header. See + * nodejs/node#26005. + */ + + if (this.enableChunkedEncoding) { + res.write(result); + res.end(); + } + else { + res.end(result); + } +} + exports.Server = Server; diff --git a/test/server-options-test.js b/test/server-options-test.js index 6665c4c38..726557fed 100644 --- a/test/server-options-test.js +++ b/test/server-options-test.js @@ -460,4 +460,99 @@ describe('SOAP Server with Options', function() { }); }); }); + it('should use chunked transfer encoding by default', function (done) { + test.server.listen(15099, null, null, function() { + test.soapServer = soap.listen(test.server, { + path: '/stockquote', + services: test.service, + xml: test.wsdl, + }, test.service, test.wsdl); + test.baseUrl = 'http://' + test.server.address().address + ":" + test.server.address().port; + + //windows return 0.0.0.0 as address and that is not + //valid to use in a request + if (test.server.address().address === '0.0.0.0' || test.server.address().address === '::') { + test.baseUrl = 'http://127.0.0.1:' + test.server.address().port; + } + + soap.createClient(test.baseUrl + '/stockquote?wsdl', function(err, client) { + assert.ifError(err); + + client.on('response', function(body, response, eid) { + var headers = response.headers; + assert.strictEqual(headers['transfer-encoding'], 'chunked'); + assert.equal(headers['content-length'], undefined); + }) + + client.SetTradePrice({ tickerSymbol: 'GOOG' }, function(err, result, body) { + assert.ifError(err); + done(); + }); + }); + }); + }); + it('should use chunked transfer encoding when enabled in options', function (done) { + test.server.listen(15099, null, null, function() { + test.soapServer = soap.listen(test.server, { + path: '/stockquote', + services: test.service, + xml: test.wsdl, + enableChunkedEncoding: true, + }, test.service, test.wsdl); + test.baseUrl = 'http://' + test.server.address().address + ":" + test.server.address().port; + + //windows return 0.0.0.0 as address and that is not + //valid to use in a request + if (test.server.address().address === '0.0.0.0' || test.server.address().address === '::') { + test.baseUrl = 'http://127.0.0.1:' + test.server.address().port; + } + + soap.createClient(test.baseUrl + '/stockquote?wsdl', function(err, client) { + assert.ifError(err); + + client.on('response', function(body, response, eid) { + var headers = response.headers; + assert.strictEqual(headers['transfer-encoding'], 'chunked'); + assert.equal(headers['content-length'], undefined); + }) + + client.SetTradePrice({ tickerSymbol: 'GOOG' }, function(err, result, body) { + assert.ifError(err); + done(); + }); + }); + }); + }); + it('should not use chunked transfer encoding when disabled in options', function (done) { + test.server.listen(15099, null, null, function() { + test.soapServer = soap.listen(test.server, { + path: '/stockquote', + services: test.service, + xml: test.wsdl, + enableChunkedEncoding: false, + }, test.service, test.wsdl); + test.baseUrl = 'http://' + test.server.address().address + ":" + test.server.address().port; + + //windows return 0.0.0.0 as address and that is not + //valid to use in a request + if (test.server.address().address === '0.0.0.0' || test.server.address().address === '::') { + test.baseUrl = 'http://127.0.0.1:' + test.server.address().port; + } + + soap.createClient(test.baseUrl + '/stockquote?wsdl', function(err, client) { + assert.ifError(err); + + client.on('response', function(body, response, eid) { + var headers = response.headers; + assert.notEqual(headers['content-length'], undefined); + assert.equal(headers['transfer-encoding'], undefined); + }) + + client.SetTradePrice({ tickerSymbol: 'GOOG' }, function(err, result, body) { + assert.ifError(err); + done(); + }); + }); + }); + }); });