Vue DevServer 代理 POST 请求超时排查记
从 TLS 握手失败到 body-parser 吞噬请求体,一个 Vue 项目中代理远程接口踩坑的完整复盘。
背景
在开发一个商户管理后台(Vue + vue-cli + webpack-dev-server)时,需要将前端对 zft.kuaidimao.com(直付通服务)的请求从直连模式改为代理模式。项目结构如下:
浏览器 → localhost:9527 → webpack-dev-server (proxy)
↓ 转发
testzft.kuaidimao.com / zft.kuaidimao.com配置看起来非常标准:
// vue.config.js
devServer: {
proxy: {
'/zft': {
target: 'http://testzft.kuaidimao.com',
changeOrigin: true,
secure: true,
pathRewrite: { '^/zft': '/index.php/api' },
}
}
}然而——所有 POST 请求都超时了。GET /mer 接口的代理却一切正常。
第一阶段:怀疑 HTTPS/TLS 问题
症状
| 测试方式 | 结果 |
|---|---|
浏览器直接访问 http://testzft.kuaidimao.com/.../area | 正常返回 |
curl 同一地址 | 正常返回 |
| 通过 Vue dev server 代理访问 | 超时 |
另一个 /mer 代理到 crmeb-test.store.lcbcity.com | 正常 |
初步判断
Node.js 版本是 v14.21.3。根据之前的经验记录:
经验回顾
Node.js 14 TLS 连接到 https://zft.kuaidimao.com 失败(ECONNRESET),升级到 16.20.2 后解决。
于是第一反应是 TLS 协议/密码套件不兼容问题。但很快发现一个矛盾点:
矛盾点
target 是 HTTP 的(http://testzft.kuaidimao.com),根本不涉及 TLS!
代理日志里出现的 https://testzft.kuaidimao.com 其实是日志代码里硬编码的字符串前缀,并不代表实际连接协议:
// 日志写法误导了我们
onProxyReq(proxyReq) {
console.log(`[Proxy-ZFT] ${proxyReq.method} https://${proxyReq.getHeader('host')}${proxyReq.path}`)
// ^^^^ 这里是写死的!
}教训
不要被日志输出误导。日志中的 URL 可能只是格式化字符串,不代表实际的网络行为。应该用 proxyReq.protocol 或直接检查 target 配置来确认实际使用的协议。
第二阶段:排除网络层
既然不是 HTTPS 问题,那就用最原始的方式验证连通性:
node -e "
const http = require('http');
const req = http.request(
{ hostname:'testzft.kuaidimao.com', path:'/index.php/api/incoming_parts/area', method:'POST' },
res => console.log('status:', res.statusCode)
);
req.on('error', e => console.error('ERROR:', e.code, e.message));
req.setTimeout(10000, () => { console.error('TIMEOUT'); req.destroy() });
req.end();
"
# 输出:status: 200 ✅关键发现
Node.js 直接发 HTTP POST 是通的! 这说明问题出在代理中间件层,而非网络本身。用原生模块发请求可以快速排除网络层问题。
第三阶段:定位真凶 —— body-parser
关键线索
项目使用了 mock-server 来模拟 API 返回数据(开发阶段无需真实后端):
// mock/mock-server.js
module.exports = app => {
// 全局注册 body-parser 中间件
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
const mockRoutes = registerRoutes(app)
// ...
}这个 mock-server 在 vue.config.js 中通过 before 钩子注入:
devServer: {
before: require('./mock/mock-server.js') // ← 先执行
}根因分析
Express/Koa 中间件的执行顺序决定了请求的处理流程:
浏览器请求
↓
webpack-dev-server 收到请求
↓
body-parser 解析 req.body(消费了请求流中的数据) ← 🔴 问题根源
↓
http-proxy-middleware 尝试转发请求
↓ 此时 req.body 已被解析为 JS 对象,原始流数据已空!
目标服务器一直等待请求体数据...
↓ 超时 ❌body-parser 会将请求体从可读流(Readable Stream)中读取并解析为 JavaScript 对象存储在 req.body 上。 一旦被消费,下游的 http-proxy-middleware 就无法再获取到原始请求数据进行转发了。
对于 GET 请求没有影响(无请求体),所以 /mer 代理的 GET 请求正常;而 POST 请求携带表单数据,就全部超时了。
修复方案
在 onProxyReq 回调中重新序列化并写入请求体:
'/zft': {
target: 'http://testzft.kuaidimao.com',
changeOrigin: true,
secure: true,
pathRewrite: { '^/zft': '/index.php/api' },
onProxyReq(proxyReq, req) {
// body-parser 消耗了 req.body,需要重新写入请求体
if (req.body && Object.keys(req.body).length > 0) {
const bodyData = require('querystring').stringify(req.body)
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData))
proxyReq.write(bodyData)
}
}
},关键点
- 使用
querystring.stringify将对象还原为application/x-www-form-urlencoded格式 - 必须更新
Content-Lengthheader,否则服务器会继续等待更多数据 - 使用
proxyReq.write()写入数据(而不是通过 options)
验证结果
::: success
重启 dev server 后,POST 请求立即返回 200,问题解决。
:::
完整排查路径图
开始排查
│
┌──────────┼──────────┐
▼ ▼
检查代理配置 浏览器直连测试
(target/pathRewrite) (确认远程可达)
│ │
└──────────┬──────────┘
▼
检查 Node.js 版本
(v14 → 怀疑 TLS 问题)
│
┌────┴────┐
▼ ▼
target 是 HTTP 日志有误导性
(TLS 无关) (硬编码 https)
│
▼
Node.js 原生 http 请求测试
(直接 POST 成功 → 排除网络)
│
▼
审查 Express 中间件栈
(发现 body-parser 全局注册)
│
▼
在 onProxyReq 中重写请求体
│
▼
解决 ✅经验总结
| 要点 | 说明 |
|---|---|
| 中间件顺序很重要 | before 钩子在 proxy 之前运行,全局中间件会影响所有路由 |
| body-parser 有副作用 | 它会消耗请求流,后续无法再次读取原始数据 |
| 日志不可轻信 | 格式化字符串可能包含与实际情况不符的信息 |
| 先隔离后深入 | 用原生 Node.js 发请求可以快速排除网络层问题 |
| GET 正常 POST 异常 | 强烈暗示与请求体处理相关的中间件问题 |
扩展思考
更好的架构建议
如果项目不再需要 mock 功能,可以直接移除 mock/mock-server.js,从根本上避免这个问题:
// vue.config.js
devServer: {
// before: require('./mock/mock-server.js'), // 注释掉即可
proxy: { /* ... */ }
}其他可能的解法
- 条件注册 body-parser:只在 mock 路由上应用,不影响其他路由
- 使用
raw-body替代:手动控制何时解析请求体 - 将代理放到独立的服务器实例:完全隔离中间件栈
本文记录于 2026 年 7 月,基于 Vue CLI 4.x + webpack-dev-server 3.x 环境。
更新日志
9a1f6-于d7674-于
