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

Tomcat 11无法接收PHP发送的multipart/form-data请求附件问题排查

Tomcat 11无法接收PHP发送的multipart/form-data请求附件问题排查

最近碰到个头疼的老项目兼容问题:一套运行多年的PHP代码,自定义构造multipart/form-data请求(包含XML字符串字段和文件附件)发送到Tomcat服务器,在Tomcat 9.x上一直正常,升级到Tomcat 11.0.9后直接崩了,折腾了好一阵才捋清楚问题,分享下排查过程和解决思路。

问题复盘

先明确下核心场景:

  • PHP端:手动拼接multipart请求体,包含_tnix(XML内容)和attachment(文件附件)两个部分
  • Java端:原Servlet直接读取InputStream,手动解析multipart边界、字段和附件内容
  • 升级Tomcat 11后遇到两个问题:
    1. Tomcat直接拦截了multipart请求 → 已通过在项目context配置中添加allowCasualMultipartParsing="true"解决
    2. 即使开启宽松解析,自定义的InputStream解析逻辑依然找不到请求边界,直接抛出异常

先排查PHP端的格式坑(Tomcat 11对标准更严格)

翻了下提供的PHP代码,发现手动构造multipart请求体存在几个隐形问题,这些在Tomcat 9中可能被宽松兼容,但Tomcat 11严格遵循RFC 7578标准,直接触发解析失败:

  1. HTTP头引号转义错误:PHP代码里用"代替双引号(比如name="_tnix"),但HTTP头中的双引号不需要HTML转义,Tomcat解析时会把字段名识别为"_tnix"而非_tnix,直接导致找不到对应字段。
  2. 手动计算Content-Length易出错:代码里手动统计请求体长度,一旦拼接时的换行、boundary长度计算偏差,Tomcat会因长度不匹配截断请求体,自然找不到边界。
  3. 二进制流处理风险:手动读取文件二进制流拼接,虽然用了rb模式,但如果文件包含特殊字节,可能和boundary冲突,或导致长度计算偏差。

PHP端修复方案:让CURL自动构造multipart请求

放弃手动拼接请求体,改用CURL原生的multipart支持,它会自动处理boundary生成、头格式、长度计算,完全符合标准:

function SendTmsNewsToHost($host, $port, $dest_url, $tnixdata, $attachment) {
    global $debug, $TMS_LineId;
    if ($port == 0) $port = 8080;
    $destination = "http://$host:$port/$dest_url";
    
    // 构造POST字段数组
    $postFields = [
        '_tnix' => $tnixdata
    ];
    
    // 处理附件(PHP 5.5+推荐用CURLFile,旧版本可改用@$attachment,但@已被弃用)
    if (!empty($attachment)) {
        $postFields['attachment'] = new CURLFile($attachment);
    }
    
    $curl = curl_init($destination);
    if (is_resource($curl)) {
        curl_setopt_array($curl, [
            CURLOPT_HTTPHEADER => [
                "Host: $host:$port",
                "Connection: Close"
            ],
            CURLOPT_FAILONERROR => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HEADER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_VERBOSE => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $postFields
        ]);
        
        $response = curl_exec($curl);
        curl_close($curl);
        return $response;
    }
    return false;
}

这个改动能从根源上解决格式兼容问题,毕竟CURL的multipart实现经过严格测试,适配所有符合标准的服务器。

Java端:放弃自定义解析,用Servlet原生API

原手动读取InputStream解析multipart的方式过于脆弱,完全依赖Tomcat旧版的宽松兼容逻辑,升级后直接失效。Servlet 3.0+提供了原生Part API,能完美适配Tomcat 11的严格解析,代码更简洁可靠。

第一步:配置Servlet支持multipart请求

推荐用注解方式配置(更直观),也可通过web.xml配置:

@WebServlet("/your-servlet-path")
@MultipartConfig(
    fileSizeThreshold = 1024 * 1024 * 2, // 2MB(超过该大小的文件会写入磁盘)
    maxFileSize = 1024 * 1024 * 10,      // 单文件最大10MB
    maxRequestSize = 1024 * 1024 * 50    // 整个请求最大50MB
)
public class YourMultipartServlet extends HttpServlet {
    // 业务逻辑代码
}

第二步:在doPost方法中直接获取字段和附件

无需再手动解析流,直接用request.getPart()获取对应字段:

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 1. 获取XML字段_tnix
    Part tnixPart = request.getPart("_tnix");
    if (tnixPart == null) {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少必填字段_tnix");
        return;
    }
    // 读取XML内容(编码根据实际业务调整)
    String xmlContent = new String(tnixPart.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
    // 处理XML业务逻辑...

    // 2. 获取附件attachment
    Part attachmentPart = request.getPart("attachment");
    if (attachmentPart != null) {
        String fileName = getSubmittedFileName(attachmentPart);
        if (fileName != null && !fileName.isBlank()) {
            // 示例:将附件保存到临时目录,根据业务调整逻辑
            try (InputStream fileIn = attachmentPart.getInputStream();
                 OutputStream fileOut = new FileOutputStream(new File("/tmp/" + fileName))) {
                fileIn.transferTo(fileOut);
            }
            // 附件业务逻辑...
        }
    }

    // 返回响应
    response.setStatus(HttpServletResponse.SC_OK);
    response.getWriter().write("请求处理成功");
}

// 兼容不同Tomcat版本的文件名获取方法
private String getSubmittedFileName(Part part) {
    String contentDisposition = part.getHeader("Content-Disposition");
    if (contentDisposition == null) return null;
    
    for (String segment : contentDisposition.split(";")) {
        segment = segment.trim();
        if (segment.startsWith("filename=")) {
            String fileName = segment.substring(9).trim();
            // 移除文件名前后的引号
            return fileName.replaceFirst("^\"", "").replaceFirst("\"$", "");
        }
    }
    return null;
}

为什么自定义解析在Tomcat 11失效?

Tomcat 11基于Servlet 6.0规范,对multipart请求的校验更严格:

  • 边界字符串的前后必须严格匹配RFC格式,不允许多余空格或换行
  • Content-Disposition头的字段名、文件名必须符合标准格式,不允许非法字符
  • 请求体的Content-Length必须与实际字节数完全一致,否则直接截断请求
    原自定义解析逻辑未考虑这些严格校验,依赖Tomcat 9的宽松兼容,升级后自然崩溃。

最后总结

  1. PHP端:永远不要手动拼接multipart请求体,交给CURL自动处理,避免格式兼容问题。
  2. Java端:放弃自定义流解析,用Servlet原生的Part API,适配所有符合标准的Tomcat版本,代码更可靠。
  3. Tomcat配置:保留allowCasualMultipartParsing="true",除非你的请求完全符合RFC 7578标准,否则Tomcat 11仍会拦截非标准请求。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 12:50:31