From 8243a5686bbec18fe2cec3578da8e80132fd201b Mon Sep 17 00:00:00 2001 From: gurpreet319 Date: Thu, 10 Sep 2026 07:40:34 +0000 Subject: [PATCH] RDKEMW-24812: DefaultMessageDispatcher stops after 5-10 minutes of playback Reason for change: Updated polyfills for fetch and xhr Test Procedure: build should be successful. Risks: low Priority: P2 --- src/jsc/modules/linkedjsdomwrapper.js | 52 +++++++++++++ src/jsc/modules/node-fetch.js | 103 ++++++++++++++++++-------- utils/xhr.js | 71 +++++++++++++++++- 3 files changed, 194 insertions(+), 32 deletions(-) diff --git a/src/jsc/modules/linkedjsdomwrapper.js b/src/jsc/modules/linkedjsdomwrapper.js index a895b69..63760c0 100755 --- a/src/jsc/modules/linkedjsdomwrapper.js +++ b/src/jsc/modules/linkedjsdomwrapper.js @@ -17,6 +17,56 @@ * limitations under the License. **/ + +if (typeof Error.captureStackTrace !== 'function') { + Error.captureStackTrace = function(targetObject, constructorOpt) { + try { + var stack = new Error().stack; + if (stack && targetObject) { + var lines = stack.split('\n'); + targetObject.stack = lines.slice(1).join('\n'); + } + if (typeof console !== 'undefined' && typeof console.log === 'function') { + var ctorName = (constructorOpt && constructorOpt.name) ? constructorOpt.name : 'unknown'; + console.log('Error.captureStackTrace called for ' + ctorName + ' (polyfilled no-op, previously would have thrown and hung the fetch promise)'); + + if (targetObject && targetObject.message) { + console.log('message: ' + targetObject.message); + } + if (targetObject && targetObject.type) { + console.log('type: ' + targetObject.type); + } + console.log('stack:\n' + (targetObject && targetObject.stack ? targetObject.stack : stack)); + } + } catch (e) { + } + }; +} + +(function ppGuardGetOwnPropertySymbols() { + if (typeof Object.getOwnPropertySymbols !== 'function') { + return; + } + var originalGetOwnPropertySymbols = Object.getOwnPropertySymbols; + if (originalGetOwnPropertySymbols.__ppGuarded) { + return; + } + var guarded = function(obj) { + if (obj === undefined || obj === null) { + try { + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log('Object.getOwnPropertySymbols called with ' + obj + ', returning [] instead of throwing'); + } + } catch (e) { + } + return []; + } + return originalGetOwnPropertySymbols(obj); + }; + guarded.__ppGuarded = true; + Object.getOwnPropertySymbols = guarded; +})(); + LinkedJSDOM = LinkedJSDOMLib; function JSDOM(html) { @@ -46,6 +96,8 @@ catch(e) XMLHttpRequest = window.XMLHttpRequest; HTMLElement = window.HTMLElement; window.location = {"href":"", "host":"127.0.0.1", "protocol":"http"} +Image = window.Image; + //below all are undefined /* diff --git a/src/jsc/modules/node-fetch.js b/src/jsc/modules/node-fetch.js index 75b2c10..b7657c0 100755 --- a/src/jsc/modules/node-fetch.js +++ b/src/jsc/modules/node-fetch.js @@ -1188,6 +1188,44 @@ const INTERNALS = Symbol('Body internals'); // fix an issue where "PassThrough" isn't a named export for node <10 const PassThrough = Stream.PassThrough; +function isStreamLike(body) { + return !!body + && typeof body.on === 'function' + && (typeof body.pipe === 'function' || typeof body.once === 'function'); +} + +function getErrorReason(err) { + if (!err) { + return 'unknown error'; + } + + if (typeof err === 'string') { + return err; + } + + if (err.message) { + return err.message; + } + + if (err.description) { + return err.description; + } + + if (err.reason) { + return err.reason; + } + + if (err.code !== undefined) { + return 'code: ' + err.code; + } + + try { + return JSON.stringify(err); + } catch (e) { + return String(err); + } +} + /** * Body mixin * @@ -1219,7 +1257,7 @@ function Body(body) { } else if (ArrayBuffer.isView(body)) { // body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); - } else if (body instanceof Stream) ; else { + } else if (body instanceof Stream || isStreamLike(body)) ; else { // none of the above // coerce to string then buffer //body = Buffer.from(String(body)); @@ -1234,7 +1272,7 @@ function Body(body) { this.size = size; this.timeout = timeout; - if (body instanceof Stream) { + if (body instanceof Stream || isStreamLike(body)) { body.on('error', function (err) { const error = err.name === 'AbortError' ? err : new FetchError(`Invalid response body while trying to fetch ${_this.url}: ${err.message}`, 'system', err); _this[INTERNALS].error = error; @@ -1289,8 +1327,12 @@ Body.prototype = { var _this2 = this; return consumeBody.call(this).then(function (buffer) { + var text = buffer.toString(); + if (text.trim() === '') { + return {}; + } try { - return JSON.parse(buffer.toString()); + return JSON.parse(text); } catch (err) { return Body.Promise.reject(new FetchError(`invalid json response body at ${_this2.url} reason: ${err.message}`, 'invalid-json')); } @@ -1390,7 +1432,7 @@ function consumeBody() { } // istanbul ignore if: should never happen - if (!(body instanceof Stream)) { + if (!(body instanceof Stream) && !isStreamLike(body)) { return Body.Promise.resolve(Buffer.alloc(0)); } @@ -1419,7 +1461,7 @@ function consumeBody() { reject(err); } else { // other errors, such as incorrect content-encoding - reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); + reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${getErrorReason(err)}`, 'system', err)); } }); @@ -1428,6 +1470,10 @@ function consumeBody() { return; } + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk); + } + if (_this4.size && accumBytes + chunk.length > _this4.size) { abort = true; reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, 'max-size')); @@ -1564,6 +1610,9 @@ function clone(instance) { // check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if (body instanceof Stream && typeof body.getBoundary !== 'function') { + if (typeof PassThrough !== 'function') { + throw new Error('cannot clone body stream: PassThrough is not available in this runtime'); + } // tee instance body p1 = new PassThrough(); p2 = new PassThrough(); @@ -2444,6 +2493,7 @@ const URL$1 = Url.URL || whatwgUrl.URL; // fix an issue where "PassThrough", "resolve" aren't a named export for node <10 const PassThrough$1 = Stream.PassThrough; +const hasPassThrough$1 = typeof PassThrough$1 === 'function'; const isDomainOrSubdomain = function isDomainOrSubdomain(destination, original) { const orig = new URL$1(original).hostname; @@ -2527,17 +2577,15 @@ function fetch(url, opts) { clearTimeout(reqTimeout); } - if (request.timeout) { - req.once('socket', function (socket) { - reqTimeout = setTimeout(function () { - reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); - finalize(); - }, request.timeout); - }); - } + if (request.timeout) { + reqTimeout = setTimeout(function () { + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); + finalize(); + }, request.timeout); + } req.on('error', function (err) { - reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); + reject(new FetchError(`request to ${request.url} failed, reason: ${getErrorReason(err)}`, 'system', err)); if (response && response.body) { destroyStream(response.body, err); @@ -2678,15 +2726,8 @@ function fetch(url, opts) { } } - //MADANA HACK - let body = undefined; - // prepare response - res.once('data', function (data) { - body = data; - }); - res.once('end', function () { - if (signal) signal.removeEventListener('abort', abortAndFinalize); - //let body = res.pipe(new PassThrough$1()); + if (signal) signal.removeEventListener('abort', abortAndFinalize); + let body = hasPassThrough$1 ? res.pipe(new PassThrough$1()) : res; const response_options = { url: request.url, @@ -2709,7 +2750,7 @@ function fetch(url, opts) { // 3. no Content-Encoding header // 4. no content response (204) // 5. content not modified response (304) - if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { + if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { response = new Response(body, response_options); resolve(response); return; @@ -2740,6 +2781,12 @@ function fetch(url, opts) { // for deflate if (codings == 'deflate' || codings == 'x-deflate') { + if (!hasPassThrough$1) { + body = body.pipe(zlib.createInflate()); + response = new Response(body, response_options); + resolve(response); + return; + } // handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = res.pipe(new PassThrough$1()); @@ -2776,11 +2823,9 @@ function fetch(url, opts) { console.warn(`[DEBUG] Content-Encoding '${codings}' detected but zlib not available for decompression`); } } - // otherwise, use response as-is - response = new Response(body, response_options); - console.log(response); - resolve(response); - }); + // otherwise, use response as-is + response = new Response(body, response_options); + resolve(response); }); writeToStream(req, request); diff --git a/utils/xhr.js b/utils/xhr.js index a5e1d77..48fee8a 100644 --- a/utils/xhr.js +++ b/utils/xhr.js @@ -22,6 +22,8 @@ XMLHttpRequest = function() { // Holds http.js objects var request; var response; + var requestTimeoutId = null; + var didTimeout = false; // Request settings var settings = {}; @@ -103,6 +105,8 @@ XMLHttpRequest = function() { this.responseXML = ""; this.status = null; this.statusText = null; + this.timeout = 0; + this.ontimeout = null; // Whether cross-site Access-Control requests should be made using // credentials such as cookies or authorization headers @@ -153,6 +157,11 @@ XMLHttpRequest = function() { request = null; } response = null; + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } + didTimeout = false; headers = {}; headersCase = {}; sendFlag = false; @@ -367,6 +376,15 @@ XMLHttpRequest = function() { return; } var doRequest = transport.request; + var redirectCount = 0; + var maxRedirects = 10; + + var clearRequestTimeout = function() { + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } + }; // Request is being sent, set send flag sendFlag = true; @@ -376,6 +394,7 @@ XMLHttpRequest = function() { // Handler for the response var responseHandler = function responseHandler(resp) { + clearRequestTimeout(); // Set response var to the response we got back // This is so it remains accessable outside this scope response = resp; @@ -383,6 +402,10 @@ XMLHttpRequest = function() { // @TODO Prevent looped redirects if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { try { + redirectCount += 1; + if (redirectCount > maxRedirects) { + throw new Error("XMLHttpRequest: maximum redirects exceeded (" + maxRedirects + ")"); + } var redirectLocation = response.headers.location; var redirectUrl = new URL(redirectLocation, settings.url); settings.url = redirectUrl.toString(); @@ -429,6 +452,7 @@ XMLHttpRequest = function() { }); response.on("end", function() { + clearRequestTimeout(); if (sendFlag) { // Discard the end event if the connection has been aborted setState(self.DONE); @@ -437,12 +461,14 @@ XMLHttpRequest = function() { }); response.on("error", function(error) { + clearRequestTimeout(); self.handleError(error); }); }; // Error handler for the request var errorHandler = function errorHandler(error) { + clearRequestTimeout(); self.handleError(error); }; @@ -456,10 +482,35 @@ XMLHttpRequest = function() { request.on("error", errorHandler); if (typeof request.on === "function") { request.on("abort", function() { + if (didTimeout) { + return; + } + clearRequestTimeout(); self.handleError(new Error("XMLHttpRequest: request aborted by transport")); }); } + if (this.timeout > 0) { + requestTimeoutId = setTimeout(function() { + if (!sendFlag) { + return; + } + didTimeout = true; + if (request) { + request.abort(); + request = null; + } + self.status = 0; + self.statusText = "timeout"; + self.responseText = ""; + errorFlag = true; + sendFlag = false; + setState(self.DONE); + self.dispatchEvent("timeout"); + self.dispatchEvent("loadend"); + }, this.timeout); + } + // Node 0.4 and later won't accept empty data. Make sure it's needed. if (data) { request.write(data); @@ -479,12 +530,17 @@ XMLHttpRequest = function() { * Called when an error is encountered to deal with it. */ this.handleError = function(error) { + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } this.status = 0; - this.statusText = error; - this.responseText = error.stack; + this.statusText = error && error.message ? error.message : String(error); + this.responseText = error && error.stack ? error.stack : this.statusText; errorFlag = true; setState(this.DONE); this.dispatchEvent('error'); + this.dispatchEvent('loadend'); }; /** @@ -498,8 +554,16 @@ XMLHttpRequest = function() { request = null; } - headers = defaultHeaders; + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } + didTimeout = false; + + headers = {}; + headersCase = {}; this.status = 0; + this.statusText = null; this.responseText = ""; this.responseXML = ""; @@ -582,3 +646,4 @@ XMLHttpRequest = function() { } }; }; +