本地localhost运行Node.js HTTPS代理返回ERR_EMPTY_RESPONSE空响应问题
问题背景
个人项目开发需要拦截本机发往外部服务器的所有HTTP/HTTPS请求(覆盖浏览器、curl等工具发起的请求),部分场景需根据请求主机名将请求重定向至自定义页面。
为验证请求拦截与转发的逻辑可行性,基于Node.js搭建简易HTTPS代理服务,预期功能为打印每个请求的相关信息后,将请求转发至原始目标地址,核心实现代码如下:
var https = require('https'); var httpProxy = require('http-proxy'); var fs = require('fs'); var path = require('path'); const options = { ssl: { key: fs.readFileSync(path.resolve(__dirname, './mycert.key'), 'utf8'), cert: fs.readFileSync(path.resolve(__dirname, './mycert.crt'), 'utf8') }, secure: false, toProxy: true } const proxy = httpProxy.createProxyServer(options); const httpsServer = https.createServer(options.ssl, (req, res) => { console.log("WEBSERVER") console.log(req.headers.host); proxy.web(req, res, { target: req.headers.host, }) }) const httpsListener = httpsServer.listen(8080); console.log("LISTENING ON 8080")
现存异常
- 将Windows系统代理设置指向该本地代理服务后,浏览器发起的所有请求均返回
ERR_EMPTY_RESPONSE错误,代理运行控制台无任何日志输出 - 直接向该代理地址发起HTTPS请求时,控制台可正常打印请求信息,但无法完成请求转发
- 不确定localhost环境是否支持搭建该类代理服务,需要可行的排查思路与解决方案
排查思路与解决方案
localhost环境完全支持搭建这类代理服务,问题出在代理实现逻辑不符合正向代理的协议规范,和运行环境无关。
核心错误原因
正向代理处理HTTP和HTTPS请求的逻辑和普通Web服务完全不同,当前实现完全没匹配代理协议要求:
- 正向代理默认以HTTP协议对外提供服务,不需要给代理服务本身提前配置SSL证书。HTTPS请求不会直接发HTTPS请求到代理,而是先发送
CONNECT方法的明文请求,要求代理建立到目标服务器的TCP隧道,隧道建立后才会走TLS加密流量。当前实现只启动了HTTPS服务,客户端发明文CONNECT请求时直接TLS握手失败,服务端收不到合法请求自然没有日志,客户端直接收到空响应。 - 没有实现
CONNECT方法的处理逻辑,无法建立HTTPS请求需要的转发隧道。 - 转发配置错误:
target参数仅传req.headers.host缺少协议前缀,且toProxy参数仅在转发目标是另一个代理服务时才需要开启,直接转发到目标站点时开启该参数会导致转发逻辑异常。 - 没有启动HTTP服务监听,系统代理转发的明文HTTP请求无法被处理。
修复步骤
- 去掉代理服务本身的SSL配置,先启动HTTP服务作为代理入口,同时处理普通HTTP请求和HTTPS的CONNECT隧道请求。
- 新增
CONNECT方法的处理逻辑:收到CONNECT请求后,和目标服务器建立TCP连接,向客户端返回隧道建立成功响应,再将客户端socket和目标服务器socket做双向管道转发,实现基础流量透传。 - 修正转发参数:HTTP请求转发时给target补全
http://协议前缀,移除错误的toProxy: true配置,添加转发错误处理避免服务崩溃。 - 如果需要打印HTTPS请求明文、做请求重定向,不能用普通透传隧道,需要实现中间人(MITM)逻辑:收到CONNECT请求后,不直接建立到目标站点的TCP隧道,而是用提前生成的自签CA动态签发当前请求域名的证书,和客户端完成TLS握手拿到明文请求内容,处理完打印、重定向逻辑后,再由代理和目标站点建立TLS连接转发请求。注意使用该模式需要提前把自签CA安装到系统、浏览器的信任根证书列表中,否则会触发证书安全警告。
最小可运行基础透传代理代码
const http = require('http'); const net = require('net'); const httpProxy = require('http-proxy'); const proxy = httpProxy.createProxyServer({ secure: false }); // 处理普通HTTP请求 const proxyServer = http.createServer((req, res) => { console.log('[HTTP请求]', req.method, req.url, `Host: ${req.headers.host}`); proxy.web(req, res, { target: `http://${req.headers.host}` }, (err) => { console.error('HTTP请求转发失败:', err.message); res.writeHead(502); res.end('Bad Gateway'); }); }); // 处理HTTPS CONNECT隧道请求 proxyServer.on('connect', (req, clientSocket, head) => { console.log('[HTTPS隧道建立]', req.url); const [targetHost, targetPort] = req.url.split(':'); const port = Number(targetPort) || 443; const serverSocket = net.connect(port, targetHost, () => { clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); serverSocket.write(head); // 双向流量透传 serverSocket.pipe(clientSocket); clientSocket.pipe(serverSocket); }); // 连接错误处理 clientSocket.on('error', () => serverSocket.destroy()); serverSocket.on('error', () => clientSocket.destroy()); }); proxyServer.listen(8080, () => { console.log('代理服务已启动,监听 127.0.0.1:8080'); });
注意:上述代码仅实现基础流量透传,HTTPS流量是加密传输的,服务端无法读取明文内容。如果需要实现HTTPS请求拦截、内容修改、重定向,需要在CONNECT处理阶段接入中间人TLS解密逻辑,可基于
node-forge等库实现动态证书签发能力。
内容的提问来源于stack exchange,提问作者Lior Dahan
相关产品推荐
相关产品推荐

