You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决动态反向代理的POST请求超时问题?

问题分析与解决方案

核心问题在于Fastify的请求体解析插件与http-proxy的流转发冲突:你注册的fastifyFormBody和fastifyMultipart会提前消费POST请求的原始数据流,导致http-proxy无法将完整的请求体转发到目标Docker容器,容器因一直等待请求体而不响应,最终触发超时。目标容器日志无POST记录也验证了这一点——请求根本没完整到达容器。

关键修复点

  1. 移除Fastify的请求体解析插件:代理场景下不需要解析请求体,直接转发原始流即可。
  2. 简化代理逻辑:去掉手动设置请求头、自定义超时等冗余操作,让http-proxy自动处理请求转发。
  3. 修正响应劫持后的处理:确保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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 00:55:54