PAYATHON 2026

http-proxy-middleware 如何拦截并修改 WebSocket 握手响应头?

数据阿明

成功的 WebSocket 握手需要在 proxyReqWs 中监听上游 ClientRequest 的 upgrade 事件,并在该事件的回调中同步修改响应头。对于底层使用 http-proxy 的实现,成功升级不会经过普通 HTTP 的 proxyRes 处理路径。

原代码还存在变量遮蔽:if 内的 const requestId 创建了新变量,外层的 requestId 始终是 undefined。因此,即使该回调执行,也不会添加 x-xyz-request-id。

为什么能转发响应,却没有 proxyRes 日志?

Node.js 为客户端请求提供独立的 upgrade 事件。连接成功升级时,响应和 socket 通过该事件交付。Node.js HTTP 文档

在所核对的 http-proxy WebSocket 实现中,代理先触发 proxyReqWs,再注册内部 upgrade 监听器。内部监听器会直接将 proxyRes.headers 写入客户端 socket,然后连接两端的数据流。因此,可以在 proxyReqWs 中提前注册监听器,在内部监听器写出握手响应前修改响应头。WebSocket 转发源码

问题没有提供依赖版本。下面沿用原代码的 on: { ... } 配置形式,以 http-proxy-middleware v3 使用的 http-proxy 实现为依据。其他版本需要确认底层事件顺序,不能直接假定相同。v3 文档

如何修改代理?

下面的代码沿用项目已有的 ProxyConfig 类型。第二个参数 reportExchange 由业务代码实现,用于调用外部服务,接收请求信息和后端响应中的请求 ID。

import { createProxyMiddleware } from 'http-proxy-middleware';
import type { IncomingMessage } from 'node:http';

type Exchange = {
  method: string | undefined;
  url: string | undefined;
  statusCode: number | undefined;
  requestId: string | undefined;
};

type ReportExchange = (event: Exchange) => Promise<void>;

export const setupProxy = (
  proxyConfig: ProxyConfig,
  reportExchange: ReportExchange,
) => {
  function inspectResponse(
    proxyRes: IncomingMessage,
    req: IncomingMessage,
  ): void {
    const value = proxyRes.headers['xyz-req-id'];
    const requestId = Array.isArray(value) ? value[0] : value;

    if (requestId !== undefined) {
      proxyRes.headers['x-xyz-request-id'] = requestId;
      delete proxyRes.headers['xyz-req-id'];
    }

    const event: Exchange = {
      method: req.method,
      url: req.url,
      statusCode: proxyRes.statusCode,
      requestId,
    };

    // Keep header changes synchronous; report separately.
    void Promise.resolve()
      .then(() => reportExchange(event))
      .catch((error: unknown) => {
        console.error('External service call failed:', error);
      });
  }

  return createProxyMiddleware({
    target: proxyConfig.endpoint,
    changeOrigin: true,
    secure: true,
    ws: true,
    on: {
      proxyReq(proxyReq) {
        proxyReq.setHeader(
          'Authorization',
          `Bearer ${proxyConfig.details.apiKey}`,
        );
      },
      proxyRes(proxyRes, req) {
        inspectResponse(proxyRes, req);
      },
      proxyReqWs(proxyReq, req) {
        proxyReq.setHeader(
          'Authorization',
          `Bearer ${proxyConfig.details.apiKey}`,
        );

        proxyReq.once('upgrade', (proxyRes) => {
          inspectResponse(proxyRes, req);
        });

        // An upstream refusal, such as HTTP 401, is not an upgrade.
        proxyReq.once('response', (proxyRes) => {
          inspectResponse(proxyRes, req);
        });
      },
      error(err, req, res) {
        console.error(
          'Proxy Error:', err.message,
          'Target:', proxyConfig.endpoint,
        );

        if ('writeHead' in res) {
          if (!res.headersSent) {
            res.writeHead(502, {
              'Content-Type': 'application/json',
            });
            res.end(JSON.stringify({ error: 'Bad Gateway' }));
          } else {
            res.destroy();
          }
        } else {
          res.destroy();
        }
      },
    },
  });
};

代码保留了三条响应处理入口:普通 HTTP 使用 proxyRes;WebSocket 成功升级使用上游请求的 upgrade;升级被拒绝并返回普通 HTTP 响应时,使用上游请求的 response。普通 HTTP 的 proxyRes 事件由另一条转发路径触发。HTTP 转发源码

示例对数组形式的请求 ID 取第一项。如果后端允许该字段包含多个有意义的值,应按接口约定调整。

server.js 需要改哪里?

将外部服务调用函数传入代理,其他启动逻辑可以保留:

// reportExchange is your implementation of the external API call.
const proxy = setupProxy(config, reportExchange);
app.use('/', proxy);

const server = http.createServer(app);
server.on('upgrade', proxy.upgrade);

reportExchange 需要按实际接口实现,并设置请求超时、检查失败响应和处理必要的重试。原问题没有提供外部服务的地址、鉴权方式或请求格式,因此无法给出真实调用参数。

如果原来的 RequestHandler 来自 Express,显式声明返回该类型会丢失代理特有的 .upgrade 类型信息。示例让 TypeScript 推断返回类型,也可以使用 http-proxy-middleware 导出的对应类型。

调用外部服务时要注意什么?

响应头必须在同步回调内修改完成。即使将监听器写成 async 并等待外部服务返回,代理的后续监听器也不会等待;执行到 await 之后时,握手响应可能已经发给客户端。

上述实现适用于记录日志、上报请求关联信息等无需阻塞握手的场景。如果外部服务的返回结果决定是否允许连接,或决定最终响应头,就需要自行控制握手转发时机,并处理超时、拒绝、socket 和 head 数据,不能直接使用这个异步上报方案。

不要修改 Sec-WebSocket-Accept 等握手必需字段,也不要把 Authorization 或 API Key 随日志和上报数据发送出去。这里只处理握手请求与响应;连接建立后的 WebSocket 消息帧需要另行处理。

如何确认修复有效?

重新建立 WebSocket 连接,在开发者工具中检查握手响应:状态应为 101;后端返回请求 ID 时,应出现 x-xyz-request-id,原来的 xyz-req-id 应消失。同时检查外部服务是否收到对应的请求信息和请求 ID。

再检查后端拒绝升级的情况,确认普通错误响应仍可转发,且没有误判为成功连接。这些是建议验证的行为,示例没有在问题所用环境中实际运行。

备注:内容仅供参考。