From cdc946fec9c41b0daffaf4d2305acb7cecf96dba Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Fri, 6 Dec 2019 13:36:34 +0800 Subject: [PATCH 1/2] Fix connection drops - Remove default connection timeout - https://github.com/nodejs/node/pull/27558. - Add client error listener. --- .../Servers/OutOfProcess/Http/HttpServer.ts | 30 +++++++++++++++++-- .../HttpNodeJSServiceIntegrationTests.cs | 29 ++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/NodeJS/Javascript/Servers/OutOfProcess/Http/HttpServer.ts b/src/NodeJS/Javascript/Servers/OutOfProcess/Http/HttpServer.ts index 76444cd..e926b39 100644 --- a/src/NodeJS/Javascript/Servers/OutOfProcess/Http/HttpServer.ts +++ b/src/NodeJS/Javascript/Servers/OutOfProcess/Http/HttpServer.ts @@ -23,8 +23,19 @@ patchLStat(); // Set by NodeJSProcessFactory let projectDir = process.cwd(); +// Create server +const server = http.createServer(serverOnRequestListener); + +// In Node.js v13+ this is the default, however for earlier versions it is 120 seconds. +server.setTimeout(0); + +// Send client error details to client for debugging +server.on('clientError', serverOnClientError); + // Start server -const server = http.createServer((req, res) => { +server.listen(parseInt(args.port), 'localhost', serverOnListeningListener); + +function serverOnRequestListener(req, res) { let bodyChunks = []; req. on('data', chunk => bodyChunks.push(chunk)). @@ -158,12 +169,25 @@ const server = http.createServer((req, res) => { respondWithError(res, error); } }); -}).listen(parseInt(args.port), 'localhost', function () { +} + +// Send error details to client for debugging - https://nodejs.org/api/http.html#http_event_clienterror +function serverOnClientError(error: Error, socket: stream.Duplex) { + let errorJson = JSON.stringify({ + errorMessage: error.message, + errorStack: error.stack + }); + + let httpResponseMessage = `HTTP/1.1 500 Internal Server Error\r\nContent-Length: ${Buffer.byteLength(errorJson, 'utf8')}\r\nContent-Type: text/html\r\n\r\n${errorJson}`; + socket.end(httpResponseMessage); +} + +function serverOnListeningListener() { // Signal to HttpNodeHost which loopback IP address (IPv4 or IPv6) and port it should make its HTTP connections on // and that we are ready to process invocations. let info = server.address() as AddressInfo; console.log(`[Jering.Javascript.NodeJS: Listening on IP - ${info.address} Port - ${info.port}]`); -}); +} function getTempIdentifier(invocationRequest: InvocationRequest): string { if (invocationRequest.newCacheIdentifier == null) { diff --git a/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs b/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs index 7f188dc..676f479 100644 --- a/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs +++ b/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs @@ -6,7 +6,11 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; using System.Text; +using System.Text.Json; using System.Threading; using Xunit; using Xunit.Abstractions; @@ -954,6 +958,31 @@ public static IEnumerable AllInvokeMethods_ReceiveAndLogStderrOutput_D }; } + [Fact] + public async void AllInvokeMethods_HandleHttpClientErrorsSuchAsMalformedRequests() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + await testSubject.InvokeFromStringAsync("module.exports = callback => callback();").ConfigureAwait(false); // Starts the Node.js process + Uri dummyEndpoint = testSubject.Endpoint; + var dummyJsonService = new JsonService(); + + // Act + using (var dummyHttpClient = new HttpClient()) + // Send a request with an invalid HTTP method. NodeJS drops the connection halfway through and fires the clientError event - https://nodejs.org/api/http.html#http_event_clienterror + using (var dummyHttpRequestMessage = new HttpRequestMessage(new HttpMethod("INVALID"), dummyEndpoint)) + using (HttpResponseMessage dummyHttpResponseMessage = await dummyHttpClient.SendAsync(dummyHttpRequestMessage).ConfigureAwait(false)) + using (Stream dummyStream = await dummyHttpResponseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + InvocationError result = await dummyJsonService.DeserializeAsync(dummyStream).ConfigureAwait(false); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, dummyHttpResponseMessage.StatusCode); + Assert.False(string.IsNullOrWhiteSpace(result.ErrorMessage)); + Assert.False(string.IsNullOrWhiteSpace(result.ErrorStack)); + } + } + /// /// Specify for access to all logging output. /// From ce10791b8651b4fda4adafa1d3afc4f923379f79 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Fri, 6 Dec 2019 21:11:24 +0800 Subject: [PATCH 2/2] Added release 5.2.1. --- Changelog.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 98ccec2..66dc8be 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,10 +3,14 @@ This project uses [semantic versioning](http://semver.org/spec/v2.0.0.html). Ref *[Semantic Versioning in Practice](https://www.jering.tech/articles/semantic-versioning-in-practice)* for an overview of semantic versioning. -## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/5.2.0...HEAD) +## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/5.2.1...HEAD) -## [5.2.0](https://github.com/JeringTech/Javascript.NodeJS/compare/5.1.1...5.2.0) - Dec 4, 2019 +## [5.2.1](https://github.com/JeringTech/Javascript.NodeJS/compare/5.2.0...5.2.1) - Dec 6, 2019 ### Fixes +- Improved HTTP connection stability and error logging. ([#61](https://github.com/JeringTech/Javascript.NodeJS/pull/61)). + +## [5.2.0](https://github.com/JeringTech/Javascript.NodeJS/compare/5.1.1...5.2.0) - Dec 4, 2019 +### Changes - Expanded API. ([#57](https://github.com/JeringTech/Javascript.NodeJS/pull/57)). Added `INodeJSService` members for invocations without return values and atomic/simplified caching-invoking: - `Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);`