From 11bcaf6ac4063934be86ab1b1f1b5f9aadccc000 Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Mon, 17 Aug 2026 17:20:18 +0530 Subject: [PATCH 1/5] route redirected requests to header modification --- .../middlewares/rules_middleware.js | 25 ++++++++ .../handle_mixed_response.js | 60 +++++++------------ .../rule_action_processor/index.js | 7 ++- .../processors/redirect_processor.js | 17 ++++-- .../rule_action_processor/utils.js | 13 +++- 5 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/components/proxy-middleware/middlewares/rules_middleware.js b/src/components/proxy-middleware/middlewares/rules_middleware.js index de76904..a611149 100644 --- a/src/components/proxy-middleware/middlewares/rules_middleware.js +++ b/src/components/proxy-middleware/middlewares/rules_middleware.js @@ -5,6 +5,10 @@ import { } from "../helpers/proxy_ctx_helper"; import RuleProcessorHelper from "../helpers/rule_processor_helper"; import RuleActionProcessor from "../rule_action_processor"; +import { PROXY_HANDLER_TYPE } from "../../../lib/proxy"; +import { RULE_ACTION } from "../constants"; +import process_modify_header_action from "../rule_action_processor/processors/modify_header_processor"; +import * as Sentry from "@sentry/browser"; class RulesMiddleware { constructor(is_active, ctx, rulesHelper) { @@ -79,6 +83,24 @@ class RulesMiddleware { return rule_actions; }; + _applyResponseHeaderRulesForRedirect = (ctx, destUrl) => { + const originalUrl = this.request_data.request_url; + const prevHandler = ctx.currentHandler; + try { + this._update_request_data({ request_url: destUrl }); // match response rules against the destination + this._init_response_data(ctx); // reads the fetched headers on ctx.serverToProxyResponse + ctx.currentHandler = PROXY_HANDLER_TYPE.ON_RESPONSE; + this._process_rules(true) + .filter((action) => action?.action === RULE_ACTION.MODIFY_HEADERS) + .forEach((action) => process_modify_header_action(action, ctx)); // mutates ctx.serverToProxyResponse.headers + } catch (e) { + Sentry.captureException(e); // degrade: serve the redirected response without the failed header mods + } finally { + ctx.currentHandler = prevHandler; + this._update_request_data({ request_url: originalUrl }); + } + }; + _update_action_result_objs = (action_result_objs = []) => { if (action_result_objs) { this.action_result_objs = @@ -96,6 +118,9 @@ class RulesMiddleware { this.on_request_actions = this._process_rules(); + ctx.rq.applyResponseHeaderRulesForRedirect = (destUrl) => + this._applyResponseHeaderRulesForRedirect(ctx, destUrl); + const { action_result_objs, continue_request } = await this.rule_action_processor.process_actions( this.on_request_actions, diff --git a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js index 290eb72..c0d52e9 100644 --- a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js +++ b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js @@ -1,47 +1,33 @@ const axios = require("axios"); -const parser = require("ua-parser-js"); import fs from "fs"; import * as Sentry from "@sentry/browser"; const mime = require('mime-types'); const handleMixedResponse = async (ctx, destinationUrl) => { - // Handling mixed response from safari - let user_agent_str = null; - user_agent_str = ctx?.clientToProxyRequest?.headers["user-agent"]; - const user_agent = parser(user_agent_str)?.browser?.name; - const LOCAL_DOMAINS = ["localhost", "127.0.0.1"]; - if (ctx.isSSL && destinationUrl.includes("http:")) { - if ( - user_agent === "Safari" || - !LOCAL_DOMAINS.some((domain) => destinationUrl.includes(domain)) - ) { - try { - const resp = await axios.get(destinationUrl, { - headers: { - "Cache-Control": "no-cache", - }, - }); - - return { - status: true, - response_data: { - headers: { "Cache-Control": "no-cache" }, - status_code: 200, - body: resp.data, - }, - }; - } catch (e) { - Sentry.captureException(e); - return { - status: true, - response_data: { - headers: { "Cache-Control": "no-cache" }, - status_code: 502, - body: e.response ? e.response.data : null, - }, - }; - } + try { + const resp = await axios.get(destinationUrl, { + responseType: "arraybuffer", // never JSON-parse; binary-safe; axios still decompresses gzip + headers: { "Cache-Control": "no-cache" }, + }); + return { + status: true, + response_data: { + status_code: resp.status, + headers: resp.headers, // real upstream headers (content-encoding already stripped by axios) + body: resp.data, // Buffer + }, + }; + } catch (e) { + Sentry.captureException(e); + return { + status: true, + response_data: { + headers: { "Cache-Control": "no-cache" }, + status_code: e.response ? e.response.status : 502, + body: e.response ? e.response.data : null, + }, + }; } } diff --git a/src/components/proxy-middleware/rule_action_processor/index.js b/src/components/proxy-middleware/rule_action_processor/index.js index f1224e3..82824b4 100644 --- a/src/components/proxy-middleware/rule_action_processor/index.js +++ b/src/components/proxy-middleware/rule_action_processor/index.js @@ -41,10 +41,11 @@ class RuleActionProcessor { const status_code = action_result.post_process_data.status_code || 200; const headers = action_result.post_process_data.headers || {}; - let body = action_result.post_process_data.body || null; + let body = action_result.post_process_data.body; + if (body === undefined) body = null; // console.log("Log", ctx.rq.original_request); - if(typeof(body) !== 'string') { + if (body !== null && !Buffer.isBuffer(body) && typeof body !== "string") { body = JSON.stringify(body); } @@ -75,7 +76,7 @@ class RuleActionProcessor { switch (rule_action.action) { case RULE_ACTION.REDIRECT: - action_result = process_redirect_action(rule_action, ctx); + action_result = await process_redirect_action(rule_action, ctx); break; case RULE_ACTION.MODIFY_HEADERS: action_result = process_modify_header_action(rule_action, ctx); diff --git a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js index adf9d31..108cd69 100644 --- a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js +++ b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js @@ -8,6 +8,7 @@ import handleMixedResponse from "../handle_mixed_response"; import { build_action_processor_response, build_post_process_data, + stripHopByHopHeaders, } from "../utils"; // adding util to get origin header for handling cors @@ -46,14 +47,20 @@ const process_redirect_action = async (action, ctx) => { ); if (isMixedResponse) { + // Feed the fetched response into the response context, then run the existing + // response Modify Headers processor against the destination URL. + ctx.serverToProxyResponse = { + statusCode: response_data.status_code, + headers: { ...(response_data.headers || {}) }, + }; + if (typeof ctx.rq.applyResponseHeaderRulesForRedirect === "function") { + ctx.rq.applyResponseHeaderRulesForRedirect(new_url); + } + const headers = stripHopByHopHeaders(ctx.serverToProxyResponse.headers); return build_action_processor_response( action, true, - build_post_process_data( - response_data.status_code, - response_data.headers, - response_data.body - ) + build_post_process_data(response_data.status_code, headers, response_data.body) ); } diff --git a/src/components/proxy-middleware/rule_action_processor/utils.js b/src/components/proxy-middleware/rule_action_processor/utils.js index d8de508..f8f3171 100644 --- a/src/components/proxy-middleware/rule_action_processor/utils.js +++ b/src/components/proxy-middleware/rule_action_processor/utils.js @@ -59,4 +59,15 @@ export const getHost = (ctx) => { export const get_file_contents = (file_path) => { return fs.readFileSync(file_path, "utf-8"); -} \ No newline at end of file +}; + +export const stripHopByHopHeaders = (headers) => { + const out = { ...(headers || {}) }; + for (const key of Object.keys(out)) { + const k = key.toLowerCase(); + if (k === "content-length" || k === "transfer-encoding") { + delete out[key]; + } + } + return out; +}; \ No newline at end of file From 3414fe02ba49f195ec19e771db8ef9d7c700a1cc Mon Sep 17 00:00:00 2001 From: kanishkrawatt Date: Mon, 17 Aug 2026 17:54:46 +0530 Subject: [PATCH 2/5] fix(redirect): relay every upstream status from the mixed-content fetch Axios rejects non-2xx by default, so a 3xx/4xx/5xx from the redirect destination landed in the catch branch, which replaced the upstream headers with a hardcoded { "Cache-Control": "no-cache" }. Response header rules then ran against that stub instead of the real response. validateStatus: () => true routes every HTTP status through the success path with its real status, headers and body. The catch branch now only sees transport failures (DNS, refused, timeout, redirect loop), and forwards e.response.headers when one is present. Redirect following is deliberately left at the axios default: the mixed-content fetch exists because the browser cannot follow an http:// hop from an https:// page, so resolving redirects server-side is the point of this path. Co-Authored-By: Claude Opus 5 --- .../rule_action_processor/handle_mixed_response.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js index c0d52e9..5266da3 100644 --- a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js +++ b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js @@ -8,6 +8,7 @@ const handleMixedResponse = async (ctx, destinationUrl) => { try { const resp = await axios.get(destinationUrl, { responseType: "arraybuffer", // never JSON-parse; binary-safe; axios still decompresses gzip + validateStatus: () => true, // relay every upstream status with its real headers instead of throwing headers: { "Cache-Control": "no-cache" }, }); return { @@ -19,11 +20,12 @@ const handleMixedResponse = async (ctx, destinationUrl) => { }, }; } catch (e) { + // Only transport failures reach here now (DNS, refused, timeout, redirect loop). Sentry.captureException(e); return { status: true, response_data: { - headers: { "Cache-Control": "no-cache" }, + headers: e.response ? e.response.headers : { "Cache-Control": "no-cache" }, status_code: e.response ? e.response.status : 502, body: e.response ? e.response.data : null, }, From 75848b6e41046bd6bff28cd5b6d8f82570f06e3c Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Mon, 17 Aug 2026 22:34:20 +0530 Subject: [PATCH 3/5] fix(proxy): resolve response header matching and stream hanging on redirects - Preserve original request context during redirect rule evaluation - Recalculate Content-Length and set decompress: false to prevent hung requests - Safely cast ArrayBuffer to Node Buffer for binary payload compatibility - Strip hop-by-hop headers from upstream redirect responses --- .../middlewares/rules_middleware.js | 13 ++++++------- .../handle_mixed_response.js | 5 +++-- .../rule_action_processor/index.js | 6 ++++++ .../processors/redirect_processor.js | 19 ++++++++++++++++++- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/components/proxy-middleware/middlewares/rules_middleware.js b/src/components/proxy-middleware/middlewares/rules_middleware.js index a611149..a08a6c6 100644 --- a/src/components/proxy-middleware/middlewares/rules_middleware.js +++ b/src/components/proxy-middleware/middlewares/rules_middleware.js @@ -84,23 +84,22 @@ class RulesMiddleware { }; _applyResponseHeaderRulesForRedirect = (ctx, destUrl) => { - const originalUrl = this.request_data.request_url; const prevHandler = ctx.currentHandler; try { - this._update_request_data({ request_url: destUrl }); // match response rules against the destination - this._init_response_data(ctx); // reads the fetched headers on ctx.serverToProxyResponse + this._init_response_data(ctx); // Reads fetched headers from ctx.serverToProxyResponse ctx.currentHandler = PROXY_HANDLER_TYPE.ON_RESPONSE; + + // Evaluates rules against the ORIGINAL request_url (e.g., https://example.com/exampleAPI) this._process_rules(true) .filter((action) => action?.action === RULE_ACTION.MODIFY_HEADERS) - .forEach((action) => process_modify_header_action(action, ctx)); // mutates ctx.serverToProxyResponse.headers + .forEach((action) => process_modify_header_action(action, ctx)); // Mutates headers } catch (e) { - Sentry.captureException(e); // degrade: serve the redirected response without the failed header mods + Sentry.captureException(e); } finally { ctx.currentHandler = prevHandler; - this._update_request_data({ request_url: originalUrl }); } }; - + _update_action_result_objs = (action_result_objs = []) => { if (action_result_objs) { this.action_result_objs = diff --git a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js index 5266da3..1d16f55 100644 --- a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js +++ b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js @@ -7,8 +7,9 @@ const handleMixedResponse = async (ctx, destinationUrl) => { if (ctx.isSSL && destinationUrl.includes("http:")) { try { const resp = await axios.get(destinationUrl, { - responseType: "arraybuffer", // never JSON-parse; binary-safe; axios still decompresses gzip - validateStatus: () => true, // relay every upstream status with its real headers instead of throwing + responseType: "arraybuffer", + decompress: false, // 2. CRITICAL: Prevent Axios from unwrapping gzip + validateStatus: () => true, headers: { "Cache-Control": "no-cache" }, }); return { diff --git a/src/components/proxy-middleware/rule_action_processor/index.js b/src/components/proxy-middleware/rule_action_processor/index.js index 82824b4..d1ec99a 100644 --- a/src/components/proxy-middleware/rule_action_processor/index.js +++ b/src/components/proxy-middleware/rule_action_processor/index.js @@ -45,6 +45,12 @@ class RuleActionProcessor { if (body === undefined) body = null; // console.log("Log", ctx.rq.original_request); + if (body instanceof ArrayBuffer) { + body = Buffer.from(body); + } else if (body && body.buffer instanceof ArrayBuffer && !Buffer.isBuffer(body)) { + body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); + } + if (body !== null && !Buffer.isBuffer(body) && typeof body !== "string") { body = JSON.stringify(body); } diff --git a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js index 108cd69..0ffbeff 100644 --- a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js +++ b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js @@ -57,10 +57,27 @@ const process_redirect_action = async (action, ctx) => { ctx.rq.applyResponseHeaderRulesForRedirect(new_url); } const headers = stripHopByHopHeaders(ctx.serverToProxyResponse.headers); + + // Recalculate content-length for the flattened buffer + let body = response_data.body; + if (body !== null && body !== undefined) { + if (Buffer.isBuffer(body)) { + headers['content-length'] = Buffer.byteLength(body); + } else if (body.byteLength !== undefined) { + headers['content-length'] = body.byteLength; + } else if (typeof body === 'string') { + headers['content-length'] = Buffer.byteLength(body); + } else { + headers['content-length'] = Buffer.byteLength(JSON.stringify(body)); + } + } else { + headers['content-length'] = 0; // Prevent hanging on empty bodies + } + return build_action_processor_response( action, true, - build_post_process_data(response_data.status_code, headers, response_data.body) + build_post_process_data(response_data.status_code, headers, body) ); } From edf90cf6cbddeaf54aa24c8a6c0702dc3b846a4c Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Mon, 17 Aug 2026 22:43:51 +0530 Subject: [PATCH 4/5] fix(proxy): track applied response header rules during redirects - Collect result objects from process_modify_header_action in _applyResponseHeaderRulesForRedirect - Register results via _update_action_result_objs so the UI accurately displays all applied rules --- .../middlewares/rules_middleware.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/proxy-middleware/middlewares/rules_middleware.js b/src/components/proxy-middleware/middlewares/rules_middleware.js index a08a6c6..4895989 100644 --- a/src/components/proxy-middleware/middlewares/rules_middleware.js +++ b/src/components/proxy-middleware/middlewares/rules_middleware.js @@ -89,10 +89,15 @@ class RulesMiddleware { this._init_response_data(ctx); // Reads fetched headers from ctx.serverToProxyResponse ctx.currentHandler = PROXY_HANDLER_TYPE.ON_RESPONSE; - // Evaluates rules against the ORIGINAL request_url (e.g., https://example.com/exampleAPI) - this._process_rules(true) - .filter((action) => action?.action === RULE_ACTION.MODIFY_HEADERS) - .forEach((action) => process_modify_header_action(action, ctx)); // Mutates headers + const modifyHeaderActions = this._process_rules(true) + .filter((action) => action?.action === RULE_ACTION.MODIFY_HEADERS); + // Process actions and collect their result objects + const actionResults = modifyHeaderActions.map((action) => + process_modify_header_action(action, ctx) + ); + + // Register results so the UI logs both rules as applied + this._update_action_result_objs(actionResults); } catch (e) { Sentry.captureException(e); } finally { From a4140790260e1f91125753628fe0bfeb07d42458 Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Mon, 17 Aug 2026 22:59:04 +0530 Subject: [PATCH 5/5] fix(proxy): refine binary type checks for Content-Length calculation - Replace duck-typed byteLength check with ArrayBuffer and ArrayBuffer.isView - Prevent Content-Length mismatch on JSON objects containing a byteLength property --- .../rule_action_processor/processors/redirect_processor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js index 0ffbeff..702fad7 100644 --- a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js +++ b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js @@ -62,8 +62,8 @@ const process_redirect_action = async (action, ctx) => { let body = response_data.body; if (body !== null && body !== undefined) { if (Buffer.isBuffer(body)) { - headers['content-length'] = Buffer.byteLength(body); - } else if (body.byteLength !== undefined) { + headers['content-length'] = body.length; + } else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { headers['content-length'] = body.byteLength; } else if (typeof body === 'string') { headers['content-length'] = Buffer.byteLength(body); @@ -71,7 +71,7 @@ const process_redirect_action = async (action, ctx) => { headers['content-length'] = Buffer.byteLength(JSON.stringify(body)); } } else { - headers['content-length'] = 0; // Prevent hanging on empty bodies + headers["content-length"] = 0; } return build_action_processor_response(