Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions dist/components/proxy-middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion dist/lib/proxy/lib/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
62 changes: 36 additions & 26 deletions src/components/proxy-middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
20 changes: 16 additions & 4 deletions src/lib/proxy/lib/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading