如何解决动态反向代理的POST请求超时问题?
问题分析与解决方案
核心问题在于Fastify的请求体解析插件与http-proxy的流转发冲突:你注册的fastifyFormBody和fastifyMultipart会提前消费POST请求的原始数据流,导致http-proxy无法将完整的请求体转发到目标Docker容器,容器因一直等待请求体而不响应,最终触发超时。目标容器日志无POST记录也验证了这一点——请求根本没完整到达容器。
关键修复点
- 移除Fastify的请求体解析插件:代理场景下不需要解析请求体,直接转发原始流即可。
- 简化代理逻辑:去掉手动设置请求头、自定义超时等冗余操作,让
http-proxy自动处理请求转发。 - 修正响应劫持后的处理:确保
proxy.web完全接管请求/响应生命周期,避免Promise包装导致的异常。
修改后的完整代码
import Fastify, { FastifyRequest } from 'fastify'; import { db } from './db'; import { routingTable } from './db/schema'; import { eq } from 'drizzle-orm'; import dotenv from 'dotenv'; import httpProxy from 'http-proxy'; dotenv.config(); const fastify = Fastify({ logger: true, // 保留bodyLimit以支持大请求,但不解析请求体 bodyLimit: 30 * 1024 * 1024 // 30MB }); // 创建代理服务器实例 const proxy = httpProxy.createProxyServer({ changeOrigin: true, xfwd: true, // 添加X-Forwarded头 proxyTimeout: 120000, // 2分钟超时 timeout: 120000 }); // 代理错误处理 proxy.on('error', function(err, req, res) { if (res.writeHead) { fastify.log.error(`代理错误: ${err.message}`); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end(`代理错误: ${err.message}`); } }); // 代理响应日志 proxy.on('proxyRes', (proxyRes, req, res) => { fastify.log.info(`收到目标服务响应,状态码: ${proxyRes.statusCode}`); }); // 全局路由处理所有请求 fastify.all('*', async (request, reply) => { const host = request.headers.host; if (!host) { return reply.status(400).send({ error: "Host头不能为空" }); } const hostParts = host.split('.'); let subdomain = hostParts.at(0) ?? null; if (!subdomain) { return reply.status(400).send({ error: "Host格式无效" }); } const useActionRunner = subdomain.includes("action"); if (useActionRunner) { subdomain = subdomain.replace('-action', ''); } // 跳过server子域名的代理 if (subdomain.toLowerCase() === 'server') { return; } // 查询数据库获取转发端口 const result = await db .select() .from(routingTable) .where(eq(routingTable.projectId, subdomain)); if (result.length === 0) { return reply.status(404).send({ error: "项目不存在" }); } const targetUrl = useActionRunner ? `http://localhost:${result[0].action_port}` : `http://localhost:${result[0].host_port}`; // 劫持响应,让http-proxy接管 reply.hijack(); // 执行代理转发 proxy.web(request.raw, reply.raw, { target: targetUrl, ignorePath: false, prependPath: false }, (err) => { if (err) { fastify.log.error(`代理转发失败: ${err.message}`); // 确保响应已结束 if (!reply.raw.writableEnded) { reply.raw.writeHead(502, { 'Content-Type': 'text/plain' }); reply.raw.end(`网关错误: ${err.message}`); } } }); }); // 启动服务 async function start() { try { await fastify.listen({ port: 8080, host: '0.0.0.0' }); } catch (err) { fastify.log.error(err); process.exit(1); } } start();
改动说明
- 移除请求体解析插件:删除
fastifyFormBody和fastifyMultipart的注册,避免消费原始请求流。 - 简化代理配置:去掉手动设置
Content-Length和Content-Type的逻辑,http-proxy会自动复制原始请求头。 - 简化路由逻辑:去掉自定义超时和Promise包装,直接通过
reply.hijack()让代理完全控制请求/响应,确保流正确传递。 - 完善错误处理:在
proxy.web的回调中检查响应是否已结束,避免重复发送响应头。
内容的提问来源于stack exchange,提问作者MrGreenyboy
相关产品推荐
相关产品推荐

