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

Java客户端发送Multipart POST请求遇HttpMediaTypeNotSupportedException排查

Java客户端发送Multipart POST请求触发HttpMediaTypeNotSupportedException,Postman却正常

项目详情

  • 主项目
    • Spring Boot版本:3.1.0
    • Java版本:17
  • QA测试项目
    • Spring Boot版本:3.1.0
    • Java版本:1.8

Java客户端代码示例

public boolean makePostRequest(final String endpoint) {

    final HttpClient client = HttpClientBuilder.create().setDefaultCookieStore(getRest().getRequestCookies()).build();
    final HttpPost conn = getRestUtil().setUpPostConnection(endpoint);

    // File Upload
    if (Objects.nonNull(getFilePathForPost())) {

        final MultipartEntityBuilder builder = MultipartEntityBuilder.create();
        builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);

        final File file = new File(getFilePathForPost());
        final FileBody fb = new FileBody(file);
        builder.addPart(getFileParamName(), fb);

        try {

            final String[] lines = getRequestPayload().split("\n");

            for (final String line : lines) {

                builder.addTextBody(line.split("=")[0], line.split("=")[1], ContentType.parse(multipartRequestPayloadType));
            }

        } catch (final NullPointerException ignored) {
            // A Post request with no payload will be done
        }

        final String boundary = "---------------" + UUID.randomUUID().toString();
        builder.setBoundary(boundary);
        conn.setHeader(CONTENT_TYPE_HEADER, ContentType.MULTIPART_FORM_DATA.getMimeType() + ";boundary=" + boundary);
        builder.setContentType(ContentType.MULTIPART_FORM_DATA);

        conn.setEntity(builder.build());
        conn.getRequestLine();
        setFilePathForPost(null);

    } else if (Objects.nonNull(getRequestPayload())) {

        conn.setHeader(CHARSET_TYPE_HEADER, CHARSET_TYPE_UTF_8);
        conn.setHeader(CONTENT_TYPE_HEADER, getRequestPayloadType());
        conn.setEntity(new StringEntity(getRequestPayload(), StandardCharsets.UTF_8));
    }

}

Spring Boot控制器代码示例

@PostMapping(path = "/partners/{partnerSid}/invoices/{invoiceSid}/partner-actions", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public InvoiceApprovalResponse approveInvoice(@PathVariable final UUID partnerSid,
                                              @PathVariable final UUID invoiceSid,
                                              @Valid @RequestPart("data") final InvoiceApprovalRequest request,
                                              @RequestPart("file") final MultipartFile partnerInvoice) {
    // ... controller logic ...
}

错误信息

org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported

观察结果

  • 已确认Java客户端中Content-Type头设置为multipart/form-data
  • 使用了正确的请求部分名称(data和file)
  • 服务器端控制器方法已正确标注consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}
  • Postman发送请求正常,推测两种请求存在细微差异

已采取步骤

  • 在客户端添加调试器,确认构造的请求无误
  • 通过curl和Postman发送相同请求均获得200响应

疑问

  1. 为何Java客户端发送请求会触发HttpMediaTypeNotSupportedException,而Postman却正常?
  2. Apache HttpClient构造请求的方式与Postman有何差异?
  3. 使用Apache HttpClient发送Multipart请求时,需注意哪些额外头或配置?
  4. 如何进一步检查Java客户端发送的原始请求,以找出与Postman请求的差异?

排查与解决方案建议

问题根源

错误提示指向application/octet-stream不被支持,核心原因是请求中某个multipart部分的Content-Type不符合控制器要求:

  • 控制器的@RequestPart("data")需要JSON格式的请求体,若客户端未显式设置该部分的Content-Type为application/json,Spring无法正确解析为InvoiceApprovalRequest对象
  • FileBody默认可能因文件类型推断失败,使用application/octet-stream,虽MultipartFile理论支持,但结合其他配置冲突会触发报错

具体修复步骤

  1. 显式设置data部分的Content-Type为application/json
    控制器要求data是JSON对象,需确保添加该部分时指定正确的Content-Type,避免依赖不确定的multipartRequestPayloadType变量:

    // 替换原循环逻辑,假设getRequestPayload()返回完整的JSON字符串对应data字段
    if (Objects.nonNull(getRequestPayload())) {
        builder.addTextBody("data", getRequestPayload(), ContentType.APPLICATION_JSON);
    }
    

    若必须按行分割,需避免JSON中的=被错误拆分:

    final String[] lines = getRequestPayload().split("\n");
    for (final String line : lines) {
        String[] parts = line.split("=", 2); // 只分割第一个=
        if (parts.length == 2) {
            // 对data字段强制设置application/json
            ContentType contentType = "data".equals(parts[0]) ? ContentType.APPLICATION_JSON : ContentType.parse(multipartRequestPayloadType);
            builder.addTextBody(parts[0], parts[1], contentType);
        }
    }
    
  2. 移除手动设置的全局Content-Type头
    MultipartEntityBuilder.build()会自动生成包含正确boundary的Content-Type头,手动设置会导致与实体内部的配置冲突:

    // 删除该行代码
    // conn.setHeader(CONTENT_TYPE_HEADER, ContentType.MULTIPART_FORM_DATA.getMimeType() + ";boundary=" + boundary);
    
  3. 显式指定FileBody的Content-Type(可选)
    若文件类型无法被自动推断,可手动设置符合实际文件类型的Content-Type,避免默认的application/octet-stream:

    // 示例:PDF文件
    FileBody fb = new FileBody(file, ContentType.APPLICATION_PDF);
    // 通用二进制文件
    FileBody fb = new FileBody(file, ContentType.DEFAULT_BINARY);
    

疑问解答

  1. Java客户端与Postman的差异
    Postman会自动为每个multipart部分设置标准Content-Type(如JSON部分设为application/json,文件按类型设置),且自动处理boundary和请求头;而Java客户端需显式配置每个细节,手动设置错误或冲突会导致解析失败。

  2. Apache HttpClient构造请求的差异
    Apache HttpClient是底层HTTP客户端,需开发者手动控制每个请求部分的Content-Type、边界、编码等;Postman模拟浏览器行为,自动遵循HTTP规范填充这些细节,适配Spring的解析逻辑。

  3. Apache HttpClient发送Multipart请求的注意事项

    • 不要手动设置全局Content-Type头,由MultipartEntityBuilder自动生成
    • 为每个请求部分显式设置对应Content-Type(尤其是JSON、文件等特殊类型)
    • 避免对复杂Payload(如JSON)进行字符串分割操作,直接传递完整内容
    • 使用HttpMultipartMode.BROWSER_COMPATIBLE确保请求格式符合浏览器标准,适配Spring的Multipart解析
  4. 检查原始请求差异的方法

    • Spring Boot日志排查:开启DEBUG日志记录完整请求内容,在application.yml中添加:
      logging:
        level:
          org.springframework.web: DEBUG
          org.apache.coyote.http11: DEBUG
      
    • 抓包对比:用Wireshark或Fiddler分别捕获Java客户端和Postman的请求,对比请求头、每个multipart部分的Content-Type、边界值及内容格式。

内容的提问来源于stack exchange,提问作者Mari Mbiru

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 18:47:01