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

使用https.request发起POST请求返回408错误的技术求助

Express反向代理POST请求408错误排查与修复

背景

基于Express开发了一个简易HTTP请求反向代理,用于将本地HTTP/HTTPS请求转发至内部部署系统(本地通过VPN连接)。代理仅将Accept-encoding头设为utf-8,其余头部均原样转发。

当前GET和HEAD请求可成功访问内部系统,但POST请求返回408错误。直接使用Postman发起相同POST请求可得到201状态码,通过代理用Postman发起该请求也能成功,因此问题出在自定义的request函数实现上。

代码功能概述

  • 代理无需使用解码包处理请求体,因已设置Accept-encoding为utf-8
  • 当do_retry标志启用时,代理的backoff函数为简单指数退避函数(测试场景未启用)

现有request函数实现

/**
 * 发送HTTP/HTTPS请求并返回Promise
 * @param {{}} config 请求配置对象
 * @param {Buffer|String|{}} data 要发送的请求体数据
 * @param {Number} retries 失败时的重试次数,默认使用request_retries变量
 * @returns {Promise.<{status: Number|undefined,status_message:String|undefined,headers:http.IncomingHttpHeaders,body:any}>} 成功时返回响应对象,失败时抛出错误
 */
const request = (config, data = null, retries = request_retries) => {
  // eslint-disable-next-line no-unused-vars
  return new Promise((resolve, reject) => {
    try {
      // 获取对应协议的请求模块
      const _agent = config.protocol === "https" ? https : http;

      delete config.protocol;

      let _data = [];
      for (let i = 0; i < retries; i++) {
        const req = _agent.request(config, (res) => {
          // 收集响应数据
          res.on("data", (chunk) => {
            _data.push(chunk);
          });

          // 处理响应结束事件
          res.on("end", async () => {
            let _body;

            try {
              if (res.headers["content-type"]?.includes("application/json")) {
                // 移除JSON语法错误相关的非法空白字符(注释掉的代码)
                // _data = _data.map((chunk) => chunk.toString().replace(/[\n\r\s\t]+/g, " "));
                _body = JSON.parse(Buffer.concat(_data).toString("utf-8"));
              }
              // 解析text/html类型的响应
              else if (res.headers["content-type"]?.includes("text/html")) {
                _body = Buffer.concat(_data).toString("utf-8");
              } else {
                // 根据响应头的编码格式解码,默认用utf-8
                _body = Buffer.concat(_data).toString(res.headers["content-encoding"] ?? "utf-8");
              }
            } catch (err) {
              _body = Buffer.concat(_data).toString();
            }

            const response = {
              status: res.statusCode,
              status_message: res.statusMessage,
              headers: res.headers,
              body: _body,
            };

            // 判断是否解析响应
            if (res.statusCode >= 200 && res.statusCode < 300) {
              resolve(response);
            } else if (do_retry) {
              console.warn(`[${new Date().toISOString()}][PROXY] ${config.method} 请求到 ${config.hostname ?? config.host + config.path} 失败,状态码 ${res.statusCode}。\n正在重试...`);
              // 调用退避函数后重试
              await backoff(i);
            } else if (i === retries - 1) {
              resolve(response);
            } else {
              resolve(response);
            }
          });

          // 超时处理
          res.on("timeout", () => {
            reject(new Error(`请求到 ${config.hostname ?? config.host + config.path} 超时。`));
          });

          res.on("error", (err) => {
            reject(err);
          });
        });

        // 根据数据类型发送请求体
        if (data && data instanceof Buffer) {
          req.write(data);
        } else if (data && typeof data === "string") {
          req.write(data, "utf-8");
        } else if (data && typeof data === "object") {
          req.write(JSON.stringify(data), "utf-8");
        }
        // 结束请求
        req.end();
      }
    } catch (err) {
      reject(err);
    }
  });
};

问题根源分析

  1. 错误的重试触发逻辑
    当前代码用for循环直接发起retries次请求,不管前一次请求是否失败,会导致同时发送多个重复请求,且所有请求的响应数据会被累积到同一个_data数组中,造成响应解析混乱。

  2. 缺失Content-Length头
    POST请求必须携带Content-Length头告诉服务器请求体的大小,否则服务器会一直等待数据直到超时,返回408错误。当前代码未设置该头,而Postman会自动添加,因此直接用Postman请求能成功。

  3. 异步回调中的await无效
    res.on("end")是普通异步回调,里面的await backoff(i)无法阻塞代码执行,且Promise会在第一次请求结束时就触发resolve,导致重试逻辑根本无法正确执行。

修复后的request函数实现

/**
 * 发送HTTP/HTTPS请求并返回Promise
 * @param {{}} config 请求配置对象
 * @param {Buffer|String|{}} data 要发送的请求体数据
 * @param {Number} retries 失败时的重试次数,默认使用request_retries变量
 * @returns {Promise.<{status: Number|undefined,status_message:String|undefined,headers:http.IncomingHttpHeaders,body:any}>} 成功时返回响应对象,失败时抛出错误
 */
const request = (config, data = null, retries = request_retries) => {
  return new Promise((resolve, reject) => {
    // 定义递归重试函数
    const attemptRequest = (currentRetry) => {
      try {
        const _agent = config.protocol === "https" ? https : http;
        // 复制配置对象,避免修改原配置
        const requestConfig = { ...config };
        delete requestConfig.protocol;

        let requestData = null;
        // 统一处理请求体为Buffer,并设置Content-Length
        if (data) {
          if (data instanceof Buffer) {
            requestData = data;
          } else if (typeof data === "string") {
            requestData = Buffer.from(data, "utf-8");
          } else if (typeof data === "object") {
            requestData = Buffer.from(JSON.stringify(data), "utf-8");
          }
          // 添加或覆盖Content-Length头
          requestConfig.headers = {
            ...requestConfig.headers,
            "Content-Length": requestData.length
          };
        }

        const req = _agent.request(requestConfig, (res) => {
          // 每个请求单独维护响应数据数组,避免污染
          const _data = [];
          res.on("data", (chunk) => {
            _data.push(chunk);
          });

          res.on("end", () => {
            let _body;
            try {
              if (res.headers["content-type"]?.includes("application/json")) {
                _body = JSON.parse(Buffer.concat(_data).toString("utf-8"));
              } else if (res.headers["content-type"]?.includes("text/html")) {
                _body = Buffer.concat(_data).toString("utf-8");
              } else {
                _body = Buffer.concat(_data).toString(res.headers["content-encoding"] ?? "utf-8");
              }
            } catch (err) {
              _body = Buffer.concat(_data).toString();
            }

            const response = {
              status: res.statusCode,
              status_message: res.statusMessage,
              headers: res.headers,
              body: _body
            };

            // 处理响应状态码
            if (res.statusCode >= 200 && res.statusCode < 300) {
              resolve(response);
            } else if (do_retry && currentRetry < retries - 1) {
              console.warn(`[${new Date().toISOString()}][PROXY] ${requestConfig.method} 请求到 ${requestConfig.hostname ?? requestConfig.host + requestConfig.path} 失败,状态码 ${res.statusCode}。\n正在重试...`);
              // 用定时器实现退避重试
              setTimeout(() => {
                attemptRequest(currentRetry + 1);
              }, backoff(currentRetry));
            } else {
              resolve(response);
            }
          });

          // 超时处理
          res.on("timeout", () => {
            if (do_retry && currentRetry < retries - 1) {
              console.warn(`[${new Date().toISOString()}][PROXY] 请求到 ${requestConfig.hostname ?? requestConfig.host + requestConfig.path} 超时。\n正在重试...`);
              setTimeout(() => {
                attemptRequest(currentRetry + 1);
              }, backoff(currentRetry));
            } else {
              reject(new Error(`请求到 ${requestConfig.hostname ?? requestConfig.host + requestConfig.path} 超时。`));
            }
          });

          // 请求错误处理
          res.on("error", (err) => {
            if (do_retry && currentRetry < retries - 1) {
              console.warn(`[${new Date().toISOString()}][PROXY] 请求到 ${requestConfig.hostname ?? requestConfig.host + requestConfig.path} 出错:${err.message}\n正在重试...`);
              setTimeout(() => {
                attemptRequest(currentRetry + 1);
              }, backoff(currentRetry));
            } else {
              reject(err);
            }
          });
        });

        // 发送请求体并结束请求
        if (requestData) {
          req.write(requestData);
        }
        req.end();
      } catch (err) {
        // 初始化请求出错时的重试逻辑
        if (do_retry && currentRetry < retries - 1) {
          console.warn(`[${new Date().toISOString()}][PROXY] 请求初始化出错:${err.message}\n正在重试...`);
          setTimeout(() => {
            attemptRequest(currentRetry + 1);
          }, backoff(currentRetry));
        } else {
          reject(err);
        }
      }
    };

    // 发起第一次请求
    attemptRequest(0);
  });
};

修复说明

  • 改用递归函数attemptRequest处理重试,仅在当前请求失败且满足重试条件时才发起下一次请求
  • 统一将请求体转换为Buffer,并计算设置Content-Length头,解决POST请求的408超时问题
  • 每个请求单独维护响应数据数组_data,避免多次请求的响应数据混淆
  • 将await backoff替换为setTimeout,适配异步回调的执行逻辑,正确实现退避重试
  • 修复重试次数判断逻辑,确保最多重试retries次

内容的提问来源于stack exchange,提问作者Adithya Shetty

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 10:45:55