diff --git a/dist/components/proxy-middleware/index.js b/dist/components/proxy-middleware/index.js index f178de8..032f7a3 100644 --- a/dist/components/proxy-middleware/index.js +++ b/dist/components/proxy-middleware/index.js @@ -103,7 +103,9 @@ class ProxyMiddlewareManager { if (modifyResponseActionExist) { const statusCode = ctx.rq_response_status_code || 404; const responseHeaders = (0, proxy_ctx_helper_1.getResponseHeaders)(ctx) || {}; - ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders); + } ctx.proxyToClientResponse.end(ctx.rq_response_body); ctx.rq.set_final_response({ status_code: statusCode, @@ -121,10 +123,12 @@ class ProxyMiddlewareManager { // Serve a clear SSL error instead of a misleading ERR_NAME_NOT_RESOLVED. if ((0, handleUnreachableAddress_1.isCertificateError)(err)) { const { status, contentType, body, errorToken } = (0, handleUnreachableAddress_1.dataToServeCertErrorPage)(host, err === null || err === void 0 ? void 0 : err.code); - ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], { - "Content-Type": contentType, - "x-rq-error": errorToken, - }); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], { + "Content-Type": contentType, + "x-rq-error": errorToken, + }); + } ctx.proxyToClientResponse.end(body); ctx.rq.set_final_response({ status_code: status, @@ -150,10 +154,12 @@ class ProxyMiddlewareManager { const isAddressUnreachable = await (0, handleUnreachableAddress_1.isAddressUnreachableError)(hostname); if (isAddressUnreachable) { const { status, contentType, body } = (0, handleUnreachableAddress_1.dataToServeUnreachablePage)(host); - ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], { - "Content-Type": contentType, - "x-rq-error": "ERR_NAME_NOT_RESOLVED" - }); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], { + "Content-Type": contentType, + "x-rq-error": "ERR_NAME_NOT_RESOLVED" + }); + } ctx.proxyToClientResponse.end(body); ctx.rq.set_final_response({ status_code: status, @@ -246,7 +252,11 @@ class ProxyMiddlewareManager { delete responseHeaders['transfer-encoding']; delete responseHeaders['Transfer-Encoding']; } - ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders); + // Node 24 commits headers on writeHead and throws on a second call; + // guard so a re-entrant write can't crash the response pipeline. + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders); + } ctx.proxyToClientResponse.write(ctx.rq_response_body); ctx.rq.set_final_response({ ...(0, proxy_ctx_helper_1.get_response_options)(ctx), diff --git a/dist/components/proxy-middleware/rule_action_processor/index.js b/dist/components/proxy-middleware/rule_action_processor/index.js index 8947148..4276fac 100644 --- a/dist/components/proxy-middleware/rule_action_processor/index.js +++ b/dist/components/proxy-middleware/rule_action_processor/index.js @@ -44,7 +44,9 @@ class RuleActionProcessor { if (typeof (body) !== 'string') { body = JSON.stringify(body); } - ctx.proxyToClientResponse.writeHead(status_code, headers).end(body); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead(status_code, headers).end(body); + } ctx.rq.request_finished = true; ctx.rq.set_final_response({ status_code: status_code, diff --git a/dist/lib/proxy/lib/proxy.js b/dist/lib/proxy/lib/proxy.js index 7d6a4be..a36247e 100644 --- a/dist/lib/proxy/lib/proxy.js +++ b/dist/lib/proxy/lib/proxy.js @@ -915,7 +915,19 @@ Proxy.prototype._onHttpServerRequest = function (isSSL, clientToProxyRequest, pr if (err) { return self._onError("ON_RESPONSEHEADERS_ERROR", ctx, err); } - ctx.proxyToClientResponse.writeHead(ctx.serverToProxyResponse.statusCode, Proxy.filterAndCanonizeHeaders(ctx.serverToProxyResponse.headers)); + // NOTE (RQ / Node 24 + Electron 42 compat): do NOT writeHead eagerly here. + // The Requestly response middleware buffers the whole body (onResponseData + // returns no chunk) and writes the FINAL response — status, rule-modified + // headers, and body — in its onResponseEnd handler. Sending headers here + // caused a double writeHead: harmless on Node 18 (the later call overrode), + // but Node 24 commits headers on the first writeHead and throws + // ERR_HTTP_HEADERS_SENT on the second, breaking interception. Eager headers + // were also wrong — they predate the rule modifications computed at response + // end. The guard keeps the generic streaming path working when no + // response-end handler will write. + if (!ctx.proxyToClientResponse.headersSent && !ctx.onResponseEndHandlers.length) { + ctx.proxyToClientResponse.writeHead(ctx.serverToProxyResponse.statusCode, Proxy.filterAndCanonizeHeaders(ctx.serverToProxyResponse.headers)); + } ctx.responseFilters.push(new ProxyFinalResponseFilter(self, ctx)); var prevResponsePipeElem = ctx.serverToProxyResponse; ctx.responseFilters.forEach(function (filter) { diff --git a/src/components/proxy-middleware/index.js b/src/components/proxy-middleware/index.js index 19c461b..d47a8e4 100644 --- a/src/components/proxy-middleware/index.js +++ b/src/components/proxy-middleware/index.js @@ -158,11 +158,13 @@ class ProxyMiddlewareManager { if (modifyResponseActionExist) { const statusCode = ctx.rq_response_status_code || 404; const responseHeaders = getResponseHeaders(ctx) || {} - ctx.proxyToClientResponse.writeHead( - statusCode, - http.STATUS_CODES[statusCode], - responseHeaders - ); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead( + statusCode, + http.STATUS_CODES[statusCode], + responseHeaders + ); + } ctx.proxyToClientResponse.end(ctx.rq_response_body); ctx.rq.set_final_response({ @@ -185,14 +187,16 @@ class ProxyMiddlewareManager { // Serve a clear SSL error instead of a misleading ERR_NAME_NOT_RESOLVED. if (isCertificateError(err)) { const { status, contentType, body, errorToken } = dataToServeCertErrorPage(host, err?.code); - ctx.proxyToClientResponse.writeHead( - status, - http.STATUS_CODES[status], - { - "Content-Type": contentType, - "x-rq-error": errorToken, - } - ); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead( + status, + http.STATUS_CODES[status], + { + "Content-Type": contentType, + "x-rq-error": errorToken, + } + ); + } ctx.proxyToClientResponse.end(body); ctx.rq.set_final_response({ status_code: status, @@ -222,14 +226,16 @@ class ProxyMiddlewareManager { const isAddressUnreachable = await isAddressUnreachableError(hostname); if (isAddressUnreachable) { const { status, contentType, body } = dataToServeUnreachablePage(host); - ctx.proxyToClientResponse.writeHead( - status, - http.STATUS_CODES[status], - { - "Content-Type": contentType , - "x-rq-error": "ERR_NAME_NOT_RESOLVED" - } - ); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead( + status, + http.STATUS_CODES[status], + { + "Content-Type": contentType , + "x-rq-error": "ERR_NAME_NOT_RESOLVED" + } + ); + } ctx.proxyToClientResponse.end(body); ctx.rq.set_final_response({ status_code: status, @@ -348,11 +354,15 @@ class ProxyMiddlewareManager { delete responseHeaders['Transfer-Encoding']; } - ctx.proxyToClientResponse.writeHead( - statusCode, - http.STATUS_CODES[statusCode], - responseHeaders, - ); + // Node 24 commits headers on writeHead and throws on a second call; + // guard so a re-entrant write can't crash the response pipeline. + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead( + statusCode, + http.STATUS_CODES[statusCode], + responseHeaders, + ); + } ctx.proxyToClientResponse.write(ctx.rq_response_body); diff --git a/src/components/proxy-middleware/rule_action_processor/index.js b/src/components/proxy-middleware/rule_action_processor/index.js index 8f8baa3..f1224e3 100644 --- a/src/components/proxy-middleware/rule_action_processor/index.js +++ b/src/components/proxy-middleware/rule_action_processor/index.js @@ -48,7 +48,9 @@ class RuleActionProcessor { body = JSON.stringify(body); } - ctx.proxyToClientResponse.writeHead(status_code, headers).end(body); + if (!ctx.proxyToClientResponse.headersSent) { + ctx.proxyToClientResponse.writeHead(status_code, headers).end(body); + } ctx.rq.request_finished = true; ctx.rq.set_final_response({ diff --git a/src/lib/proxy/lib/proxy.ts b/src/lib/proxy/lib/proxy.ts index de79fd4..d315cd0 100644 --- a/src/lib/proxy/lib/proxy.ts +++ b/src/lib/proxy/lib/proxy.ts @@ -1080,10 +1080,22 @@ Proxy.prototype._onHttpServerRequest = function ( if (err) { return self._onError("ON_RESPONSEHEADERS_ERROR", ctx, err); } - ctx.proxyToClientResponse.writeHead( - ctx.serverToProxyResponse.statusCode, - Proxy.filterAndCanonizeHeaders(ctx.serverToProxyResponse.headers) - ); + // NOTE (RQ / Node 24 + Electron 42 compat): do NOT writeHead eagerly here. + // The Requestly response middleware buffers the whole body (onResponseData + // returns no chunk) and writes the FINAL response — status, rule-modified + // headers, and body — in its onResponseEnd handler. Sending headers here + // caused a double writeHead: harmless on Node 18 (the later call overrode), + // but Node 24 commits headers on the first writeHead and throws + // ERR_HTTP_HEADERS_SENT on the second, breaking interception. Eager headers + // were also wrong — they predate the rule modifications computed at response + // end. The guard keeps the generic streaming path working when no + // response-end handler will write. + if (!ctx.proxyToClientResponse.headersSent && !ctx.onResponseEndHandlers.length) { + ctx.proxyToClientResponse.writeHead( + ctx.serverToProxyResponse.statusCode, + Proxy.filterAndCanonizeHeaders(ctx.serverToProxyResponse.headers) + ); + } ctx.responseFilters.push(new ProxyFinalResponseFilter(self, ctx)); var prevResponsePipeElem = ctx.serverToProxyResponse; ctx.responseFilters.forEach(function (filter) {